Compare commits

..

20 Commits

Author SHA1 Message Date
Andras Bacsai 794025aee1 Changes auto-committed by Conductor (#25) 2025-10-17 11:14:24 +02:00
Andras Bacsai 8d6da93aa1 Merge pull request #24 from coollabsio/andrasbacsai/show-config-path
Add coolify config command
2025-10-17 11:05:57 +02:00
Andras Bacsai 22516fe51e Changes auto-committed by Conductor 2025-10-17 11:00:09 +02:00
Andras Bacsai fe63c3a3b6 Changes auto-committed by Conductor 2025-10-17 10:59:12 +02:00
Andras Bacsai 6cad3ba0f7 Merge pull request #23 from coollabsio/cli-ux-improvements
Refactor: Use 'context' instead of 'instance' terminology
2025-10-17 10:46:51 +02:00
Andras Bacsai 1bc4625ef8 fix: properly hide sensitive data in all commands
- Add sensitive:"true" tag to Instance.Token field
- Add sensitive:"true" tag to GitHub ClientSecret and WebhookSecret
- Add sensitive:"true" tag to EnvironmentVariable Value and RealValue
- Tokens now hidden by default, shown with --show-sensitive flag

This ensures sensitive data like API tokens, secrets, and environment
variable values are masked (********) by default in table output and
only shown when the user explicitly uses the --show-sensitive flag.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 09:27:13 +02:00
Andras Bacsai d9494301f6 fix: add trailing newline to table output
Fixes the zsh prompt indicator (%) appearing after table output
by ensuring all table output ends with a newline character.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 09:21:48 +02:00
Andras Bacsai 85cf8f6981 Merge branch 'v4.x' into cli-ux-improvements
Resolved conflicts in README.md and database commands.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 09:19:11 +02:00
Andras Bacsai c98a8330f8 Changes auto-committed by Conductor 2025-10-17 09:13:39 +02:00
Andras Bacsai 49e5870c86 Merge pull request #22 from YaRissi/fix/missing-flags
Fix: missing flags for some commands
2025-10-17 09:13:16 +02:00
YaRissi 705fed3560 fix: update README to reflect context management commands and terminology 2025-10-17 01:21:52 +02:00
YaRissi 88d0b25537 fix: correct flag names for local backup retention in create command and update README 2025-10-17 01:10:28 +02:00
YaRissi 3470b04235 fix: add missing flags to commands 2025-10-17 00:54:05 +02:00
Andras Bacsai eb876e4bda Merge pull request #21 from YaRissi/folder-file-structure
Proposal: Restructure of commands
2025-10-17 00:19:02 +02:00
Andras Bacsai 7210b8df76 refactor: Rename instances to contexts and add update command 2025-10-17 00:09:48 +02:00
YaRissi 0546fd1932 fix: autocompletion docs 2025-10-16 23:22:28 +02:00
YaRissi e2c1c86194 fix: team models 2025-10-16 23:08:16 +02:00
YaRissi 84e7e4921b fix: move command to get server domains by UUID and remove non existing domain endpoint 2025-10-16 22:55:53 +02:00
YaRissi 0acb1fc512 fix: docs command 2025-10-16 22:34:20 +02:00
YaRissi eaf9614bcc Refactor CLI commands files
- Introduced new structured command files for teams: current, get, list, and members.
- Created a new CLI client helper for API interactions.
- Removed deprecated version command and replaced it with a new structure.
- Moved utility functions from root to cli folder in internal
2025-10-16 22:29:02 +02:00
141 changed files with 6539 additions and 5611 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
coolify-cli
coolify
cli
config.json
.claude
# Generated documentation (can be regenerated)
man/
+1 -1
View File
@@ -96,7 +96,7 @@ The Coolify CLI is a command-line interface for managing Coolify instances, serv
- `root.go` - Root command, global flags, initialization
- `servers.go` - Server management commands
- `deploy.go` - Deployment commands
- `instances.go` - Instance configuration commands
- `context.go` - Context (instance) configuration commands
- `projects.go` - Project listing and inspection
- etc.
+1 -1
View File
@@ -24,7 +24,7 @@ The codebase follows Cobra's command pattern with a root command and subcommands
- Entry point: `main.go` calls `cmd.Execute()`
- Root command: `cmd/root.go` - contains core utilities (HTTP client, authentication, version checking, config management)
- Subcommands: Each command is in its own file in `cmd/`:
- `instances.go` - manage Coolify instances (add, remove, list, set default/token)
- `context.go` - manage Coolify context (add, remove, list, set default/token)
- `servers.go` - list and get server information
- `projects.go` - list projects with environments and applications
- `resources.go` - list resources
+113 -61
View File
@@ -10,48 +10,66 @@ It will install the CLI in `/usr/local/bin/coolify` and the configuration file i
> If you are a windows or mac user, please test the installation script and let us know if it works for you.
## Configuration
## Getting Started
1. Get a `<token>` from your Coolify dashboard (Cloud or self-hosted) at `/security/api-tokens`
### Cloud
2. Add the token with `coolify instances set token cloud <token>`
2. Add the token with `coolify context set-token cloud <token>`
### Self-hosted
2. Add the token with `coolify instances add -d <name> <fqdn> <token>`
> Replace `<name>` with the name you want to give to the instance.
2. Add the token with `coolify context add -d <context_name> <url> <token>`
> Replace `<context_name>` with the name you want to give to the context.
>
> Replace `<fqdn>` with the fully qualified domain name of your Coolify instance.
> Replace `<url>` with the fully qualified domain name of your Coolify instance.
Now you can use the CLI with the token you just added.
## Change default instance
You can change the default instance with `coolify instances set default <name>`
## Change default context
You can change the default context with `coolify context use <context_name>` or `coolify context set-default <context_name>`
## Currently Supported Commands
### Update
- `coolify update` - Update the CLI to the latest version
### Instances
- `coolify instances list` - List all instances
- `coolify instances add` - Create a new instance configuration
- `coolify instances remove` - Remove an instance configuration
- `coolify instances get` - Get an instance configuration
- `coolify instances set <default>|<token>` - Set an instance as default or set a token for an instance
- `coolify instances version` - Get the version of the Coolify API for an instance
### Configuration
- `coolify config` - Show configuration file location
### Context Management
- `coolify context list` - List all configured contexts
- `coolify context add <context_name> <url> <token>` - Add a new context
- `-d, --default` - Set as default context
- `-f, --force` - Force overwrite if context already exists
- `coolify context delete <context_name>` - Delete a context
- `coolify context get <context_name>` - Get details of a specific context
- `coolify context set-token <context_name> <token>` - Update the API token for a context
- `coolify context set-default <context_name>` - Set a context as the default
- `coolify context update <context_name>` - Update a context's properties
- `--name <new_name>` - Change the context name
- `--url <new_url>` - Change the context URL
- `--token <new_token>` - Change the context token
- `coolify context use <context_name>` - Switch to a different context (set as default)
- `coolify context version` - Get the Coolify API version of the current context
### Servers
- `coolify servers list` - List all servers
- `coolify servers get <uuid>` - Get a server by UUID
- `--resources` - Get the resources and their status of a server
- `coolify servers add <name> <ip> <private_key_uuid>` - Add a new server
<<<<<<< HEAD
- `coolify servers add <server_name> <ip_address> <private_key_uuid>` - Add a new server
- `--port <port>` - SSH port (default: 22)
- `--user <user>` - SSH user (default: root)
=======
- `coolify servers add <name> <ip> <private_key_uuid>` - Add a new server
- `-p, --port <port>` - SSH port (default: 22)
- `-u, --user <user>` - SSH user (default: root)
>>>>>>> origin/v4.x
- `--validate` - Validate server immediately after adding
- `coolify servers remove <uuid>` - Remove a server
- `coolify servers validate <uuid>` - Validate a server connection
- `coolify servers domains <uuid>` - Get server domains by UUID
### Projects
- `coolify projects list` - List all projects
@@ -66,8 +84,23 @@ You can change the default instance with `coolify instances set default <name>`
- `coolify app update <uuid>` - Update application configuration
- `--name <name>` - Application name
- `--description <description>` - Application description
- `--git-branch <branch>` - Git branch
- `--git-repository <url>` - Git repository URL
- `--domains <domains>` - Domains (comma-separated)
- `--build-command <cmd>` - Build command
- `--start-command <cmd>` - Start command
- `--install-command <cmd>` - Install command
- `--base-directory <path>` - Base directory
- `--publish-directory <path>` - Publish directory
- `--dockerfile <content>` - Dockerfile content
- `--docker-image <image>` - Docker image name
- `--docker-tag <tag>` - Docker image tag
- `--ports-exposes <ports>` - Exposed ports
- `--ports-mappings <mappings>` - Port mappings
- `--health-check-enabled` - Enable health check
- `--health-check-path <path>` - Health check path
- `coolify app delete <uuid>` - Delete an application
- `--force` - Skip confirmation prompt
- `-f, --force` - Skip confirmation prompt
- `coolify app start <uuid>` - Start an application
- `coolify app stop <uuid>` - Stop an application
- `coolify app restart <uuid>` - Restart an application
@@ -79,11 +112,10 @@ You can change the default instance with `coolify instances set default <name>`
- `coolify app env create <app_uuid>` - Create a new environment variable
- `--key <key>` - Variable key (required)
- `--value <value>` - Variable value (required)
- `--is-preview` - Set variable for preview environments
- `--is-build-time` - Set variable as build-time variable
- `--is-literal` - Treat value as literal (no variable expansion)
- `--is-multiline` - Allow multiline values
- `--is-shown-once` - Show value only once (for secrets)
- `--preview` - Available in preview deployments
- `--build-time` - Available at build time
- `--is-literal` - Treat value as literal (don't interpolate variables)
- `--is-multiline` - Value is multiline
- `coolify app env update <app_uuid> <env_uuid>` - Update an environment variable
- `coolify app env delete <app_uuid> <env_uuid>` - Delete an environment variable
- `coolify app env sync <app_uuid>` - Sync environment variables from a .env file
@@ -96,18 +128,24 @@ You can change the default instance with `coolify instances set default <name>`
- Supported types: `postgresql`, `mysql`, `mariadb`, `mongodb`, `redis`, `keydb`, `clickhouse`, `dragonfly`
- `--server-uuid <uuid>` - Server UUID (required)
- `--project-uuid <uuid>` - Project UUID (required)
- `--environment-name <name>` - Environment name (required unless using --environment-uuid)
- `--environment-uuid <uuid>` - Environment UUID (required unless using --environment-name)
- `--destination-uuid <uuid>` - Destination UUID if server has multiple destinations
- `--name <name>` - Database name
- `--description <description>` - Database description
- `--image <image>` - Docker image
- `--instant-deploy` - Deploy immediately after creation
- `--is-public` - Make database publicly accessible
- `--public-port <port>` - Public port number
- `--limits-memory <size>` - Memory limit (e.g., '512m', '2g')
- `--limits-cpus <cpus>` - CPU limit (e.g., '0.5', '2')
- Database-specific flags (postgres-user, mysql-root-password, etc.)
- `coolify database update <uuid>` - Update database configuration
- `coolify database delete <uuid>` - Delete a database
- `--delete-configurations` - Delete configurations (default: true)
- `--delete-volumes` - Delete volumes (default: true)
- `--docker-cleanup` - Run docker cleanup (default: true)
- `--delete-connected-networks` - Delete connected networks (default: true)
- `coolify database start <uuid>` - Start a database
- `coolify database stop <uuid>` - Stop a database
- `coolify database restart <uuid>` - Restart a database
@@ -119,9 +157,16 @@ You can change the default instance with `coolify instances set default <name>`
- `--enabled` - Enable backup schedule
- `--save-s3` - Save backups to S3
- `--s3-storage-uuid <uuid>` - S3 storage UUID
- `--retention-amount-locally <n>` - Number of backups to retain locally
- `--retention-days-locally <n>` - Days to retain backups locally
- `--timeout <seconds>` - Backup timeout
- `--databases-to-backup <list>` - Comma-separated list of databases to backup
- `--dump-all` - Dump all databases
- `--retention-amount-local <n>` - Number of backups to retain locally
- `--retention-days-local <n>` - Days to retain backups locally
- `--retention-storage-local <size>` - Max storage for local backups (e.g., '1GB', '500MB')
- `--retention-amount-s3 <n>` - Number of backups to retain in S3
- `--retention-days-s3 <n>` - Days to retain backups in S3
- `--retention-storage-s3 <size>` - Max storage for S3 backups (e.g., '1GB', '500MB')
- `--timeout <seconds>` - Backup timeout in seconds
- `--disable-local` - Disable local backup storage
- `coolify database backup update <database_uuid> <backup_uuid>` - Update a backup configuration
- `coolify database backup delete <database_uuid> <backup_uuid>` - Delete a backup configuration
- `coolify database backup trigger <database_uuid> <backup_uuid>` - Trigger an immediate backup
@@ -148,51 +193,54 @@ You can change the default instance with `coolify instances set default <name>`
### Deployments
- `coolify deploy uuid <uuid>` - Deploy a resource by UUID
<<<<<<< HEAD
- `--force` - Force deployment
- `coolify deploy name <resource_name>` - Deploy a resource by name
- `--force` - Force deployment
=======
- `-f, --force` - Force deployment
- `coolify deploy name <name>` - Deploy a resource by name
- `--force` - Force deployment
- `-f, --force` - Force deployment
>>>>>>> origin/v4.x
- `coolify deploy batch <name1,name2,...>` - Deploy multiple resources at once
- `--force` - Force all deployments
- `-f, --force` - Force all deployments
- `coolify deploy list` - List all deployments
- `coolify deploy get <uuid>` - Get deployment details
- `coolify deploy cancel <uuid>` - Cancel a deployment
- `--force` - Skip confirmation prompt
- `-f, --force` - Skip confirmation prompt
### GitHub Apps
- `coolify github list` - List all GitHub App integrations
- `coolify github get <app_uuid>` - Get GitHub App details
- `coolify github create` - Create a new GitHub App integration
- `--name <name>` - GitHub App name (required)
- `--api-url <url>` - GitHub API URL (required)
- `--html-url <url>` - GitHub HTML URL (required)
- `--api-url <url>` - GitHub API URL (required, e.g., https://api.github.com)
- `--html-url <url>` - GitHub HTML URL (required, e.g., https://github.com)
- `--app-id <id>` - GitHub App ID (required)
- `--installation-id <id>` - Installation ID (required)
- `--client-id <id>` - OAuth Client ID (required)
- `--client-secret <secret>` - OAuth Client Secret (required)
- `--private-key-uuid <uuid>` - Private key UUID (required)
- `--installation-id <id>` - GitHub Installation ID (required)
- `--client-id <id>` - GitHub OAuth Client ID (required)
- `--client-secret <secret>` - GitHub OAuth Client Secret (required)
- `--private-key-uuid <uuid>` - UUID of existing private key (required)
- `--organization <org>` - GitHub organization
- `--custom-user <user>` - Custom SSH user
- `--custom-port <port>` - Custom SSH port
- `--webhook-secret <secret>` - Webhook secret
- `--system-wide` - System-wide installation
- `--custom-user <user>` - Custom user for SSH (default: git)
- `--custom-port <port>` - Custom port for SSH (default: 22)
- `--webhook-secret <secret>` - GitHub Webhook Secret
- `--system-wide` - Is this app system-wide (cloud only)
- `coolify github update <app_uuid>` - Update a GitHub App
- `coolify github delete <app_uuid>` - Delete a GitHub App
- `--force` - Skip confirmation prompt
- `-f, --force` - Skip confirmation prompt
- `coolify github repos <app_uuid>` - List repositories accessible by a GitHub App
- `coolify github branches <app_uuid> <owner/repo>` - List branches for a repository
### Teams
- `coolify team list` - List all teams
- `coolify team get <id>` - Get team details
- `coolify team get <team_id>` - Get team details
- `coolify team current` - Get current team
- `coolify team members list [team_id]` - List team members
### Domains
- `coolify domains list` - List all domains
### Private Keys
- `coolify privatekeys list` - List all private keys
- `coolify privatekeys create <name> <private-key>` - Create a new private key
- `coolify privatekeys create <key_name> <private-key>` - Create a new private key
- Use `@filename` to read from file: `coolify privatekeys create mykey @~/.ssh/id_rsa`
- `coolify privatekeys delete <uuid>` - Delete a private key
@@ -200,12 +248,16 @@ You can change the default instance with `coolify instances set default <name>`
All commands support these global flags:
- `--instance <name>` - Use a specific instance profile instead of default (NEW)
<<<<<<< HEAD
- `--context <name>` - Use a specific context instead of default
=======
- `--instance <name>` - Use a specific instance profile instead of default
>>>>>>> origin/v4.x
- `--host <fqdn>` - Override the Coolify instance hostname
- `--token <token>` - Override the authentication token
- `--format <format>` - Output format: `table` (default), `json`, or `pretty`
- `--show-sensitive` / `-s` - Show sensitive information (tokens, IPs, etc.)
- `--force` / `-f` - Force operation (skip confirmations)
- `-s, --show-sensitive` - Show sensitive information (tokens, IPs, etc.)
- `-f, --force` - Force operation (skip confirmations)
- `--debug` - Enable debug mode
## Examples
@@ -213,20 +265,20 @@ All commands support these global flags:
### Multi-Environment Workflows
```bash
# Add multiple instances
coolify instances add prod https://prod.coolify.io <prod-token>
coolify instances add staging https://staging.coolify.io <staging-token>
coolify instances add dev https://dev.coolify.io <dev-token>
# Add multiple contexts
coolify context add prod https://prod.coolify.io <prod-token>
coolify context add staging https://staging.coolify.io <staging-token>
coolify context add dev https://dev.coolify.io <dev-token>
# Set default
coolify instances set default prod
coolify context use prod
# Use different profiles
coolify --instance=staging servers list
coolify --instance=prod deploy name api
coolify --instance=dev resources list
# Use different contexts
coolify --context=staging servers list
coolify --context=prod deploy name api
coolify --context=dev resources list
# Default profile (prod in this case)
# Default context (prod in this case)
coolify servers list
```
@@ -307,8 +359,8 @@ coolify deploy name my-application
# Deploy multiple apps at once
coolify deploy batch api,worker,frontend
# Force deploy with specific profile
coolify --instance=prod deploy batch api,worker --force
# Force deploy with specific context
coolify --context=prod deploy batch api,worker --force
# Traditional UUID deployment still works
coolify deploy uuid abc123-def456-...
@@ -365,7 +417,7 @@ coolify team members list
```bash
# List servers in production
coolify --instance=prod servers list
coolify --context=prod servers list
# Add a server with validation
coolify servers add myserver 192.168.1.100 <key-uuid> --validate
@@ -394,7 +446,7 @@ coolify servers list --format=pretty
This CLI follows a clean architecture with:
- **Service Layer**: Business logic and API interactions
- **Output Layer**: Consistent formatting across all commands
- **Config Layer**: Multi-instance configuration management
- **Config Layer**: Multi-context configuration management
- **Models Layer**: Type-safe data structures
## Development
+44
View File
@@ -0,0 +1,44 @@
package application
import (
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/cmd/application/env"
)
// NewAppCommand creates the app parent command
func NewAppCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "app",
Aliases: []string{"apps", "application", "applications"},
Short: "Application related commands",
Long: `Manage Coolify applications - list, get, create, update, delete, and control application lifecycle.`,
}
// Add main subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewUpdateCommand())
cmd.AddCommand(NewDeleteCommand())
cmd.AddCommand(NewStartCommand())
cmd.AddCommand(NewStopCommand())
cmd.AddCommand(NewRestartCommand())
cmd.AddCommand(NewLogsCommand())
// Add env subcommand with its children
envCmd := &cobra.Command{
Use: "env",
Aliases: []string{"envs", "environment"},
Short: "Manage application environment variables",
Long: `List and manage environment variables for applications. All commands require the application UUID first to establish context.`,
}
envCmd.AddCommand(env.NewListEnvCommand())
envCmd.AddCommand(env.NewGetEnvCommand())
envCmd.AddCommand(env.NewCreateEnvCommand())
envCmd.AddCommand(env.NewUpdateEnvCommand())
envCmd.AddCommand(env.NewDeleteEnvCommand())
envCmd.AddCommand(env.NewSyncEnvCommand())
cmd.AddCommand(envCmd)
return cmd
}
+53
View File
@@ -0,0 +1,53 @@
package application
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewDeleteCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete <uuid>",
Short: "Delete an application",
Long: `Delete an application. This action cannot be undone.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
force, _ := cmd.Flags().GetBool("force")
if !force {
var response string
fmt.Printf("Are you sure you want to delete application %s? This cannot be undone. (yes/no): ", uuid)
fmt.Scanln(&response)
if response != "yes" && response != "y" {
fmt.Println("Delete cancelled.")
return nil
}
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
err = appSvc.Delete(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to delete application: %w", err)
}
fmt.Printf("Application %s deleted successfully.\n", uuid)
return nil
},
}
cmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
return cmd
}
+79
View File
@@ -0,0 +1,79 @@
package env
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewCreateEnvCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create <app_uuid>",
Short: "Create an environment variable for an application",
Long: `Create a new environment variable for a specific application. Use --key and --value flags to specify the variable.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
key, _ := cmd.Flags().GetString("key")
value, _ := cmd.Flags().GetString("value")
isBuildTime, _ := cmd.Flags().GetBool("build-time")
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
if key == "" {
return fmt.Errorf("--key is required")
}
if value == "" {
return fmt.Errorf("--value is required")
}
req := &models.EnvironmentVariableCreateRequest{
Key: key,
Value: value,
}
if cmd.Flags().Changed("build-time") {
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("is-multiline") {
req.IsMultiline = &isMultiline
}
appSvc := service.NewApplicationService(client)
env, err := appSvc.CreateEnv(ctx, uuid, req)
if err != nil {
return fmt.Errorf("failed to create environment variable: %w", err)
}
fmt.Printf("Environment variable '%s' created successfully.\n", env.Key)
fmt.Printf("UUID: %s\n", env.UUID)
return nil
},
}
cmd.Flags().String("key", "", "Environment variable key (required)")
cmd.Flags().String("value", "", "Environment variable value (required)")
cmd.Flags().Bool("build-time", false, "Available at build time")
cmd.Flags().Bool("preview", false, "Available in preview deployments")
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
return cmd
}
+55
View File
@@ -0,0 +1,55 @@
package env
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewDeleteEnvCommand() *cobra.Command {
deleteEnvCmd := &cobra.Command{
Use: "delete <app_uuid> <env_uuid>",
Short: "Delete an environment variable",
Long: `Delete an environment variable from an application. First UUID is the application, second is the specific environment variable to delete.`,
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
envUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to delete this environment variable? (yes/no): ")
fmt.Scanln(&response)
if response != "yes" && response != "y" {
fmt.Println("Delete cancelled.")
return nil
}
}
appSvc := service.NewApplicationService(client)
err = appSvc.DeleteEnv(ctx, appUUID, envUUID)
if err != nil {
return fmt.Errorf("failed to delete environment variable: %w", err)
}
fmt.Println("Environment variable deleted successfully.")
return nil
},
}
deleteEnvCmd.Flags().Bool("force", false, "Skip confirmation prompt")
return deleteEnvCmd
}
+56
View File
@@ -0,0 +1,56 @@
package env
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewGetEnvCommand() *cobra.Command {
return &cobra.Command{
Use: "get <app_uuid> <env_uuid_or_key>",
Short: "Get environment variable details",
Long: `Get detailed information about a specific environment variable by UUID or key name.`,
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
envUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
env, err := appSvc.GetEnv(ctx, appUUID, envUUID)
if err != nil {
return fmt.Errorf("failed to get environment variable: %w", err)
}
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
if !showSensitive {
env.Value = "********"
if env.RealValue != nil {
masked := "********"
env.RealValue = &masked
}
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(env)
},
}
}
+57
View File
@@ -0,0 +1,57 @@
package env
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewListEnvCommand() *cobra.Command {
return &cobra.Command{
Use: "list <app_uuid>",
Short: "List all environment variables for an application",
Long: `List all environment variables for a specific application.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
envs, err := appSvc.ListEnvs(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to list environment variables: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
if !showSensitive {
for i := range envs {
envs[i].Value = "********"
if envs[i].RealValue != nil {
masked := "********"
envs[i].RealValue = &masked
}
}
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(envs)
},
}
}
+154
View File
@@ -0,0 +1,154 @@
package env
import (
"context"
"fmt"
"strings"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/parser"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewSyncEnvCommand() *cobra.Command {
syncEnvCmd := &cobra.Command{
Use: "sync <app_uuid>",
Short: "Sync environment variables from a .env file",
Long: `Sync environment variables from a .env file. This command intelligently:
- Updates existing environment variables with new values
- Creates new environment variables that don't exist yet
- Uses efficient bulk operations where possible
Example: coolify app env sync abc123 --file .env.production`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
filePath, _ := cmd.Flags().GetString("file")
if filePath == "" {
return fmt.Errorf("--file is required")
}
isBuildTime, _ := cmd.Flags().GetBool("build-time")
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
// Parse the .env file
envVars, err := parser.ParseEnvFile(filePath)
if err != nil {
return fmt.Errorf("failed to parse .env file: %w", err)
}
if len(envVars) == 0 {
fmt.Println("No environment variables found in file.")
return nil
}
fmt.Printf("Found %d environment variables in file. Syncing...\n", len(envVars))
// Fetch existing environment variables
appSvc := service.NewApplicationService(client)
existingEnvs, err := appSvc.ListEnvs(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to list existing environment variables: %w", err)
}
// Build a map of existing env vars by key
existingMap := make(map[string]models.EnvironmentVariable)
for _, env := range existingEnvs {
existingMap[env.Key] = env
}
// Separate into updates and creates
var toUpdate []models.EnvironmentVariableCreateRequest
var toCreate []models.EnvironmentVariableCreateRequest
for _, envVar := range envVars {
req := models.EnvironmentVariableCreateRequest{
Key: envVar.Key,
Value: envVar.Value,
}
// Apply flags if explicitly provided
if cmd.Flags().Changed("build-time") {
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
req.IsLiteral = &isLiteral
}
// Auto-detect multiline values
if strings.Contains(envVar.Value, "\n") {
multiline := true
req.IsMultiline = &multiline
}
if _, exists := existingMap[envVar.Key]; exists {
toUpdate = append(toUpdate, req)
} else {
toCreate = append(toCreate, req)
}
}
updateCount := 0
createCount := 0
failCount := 0
// Perform bulk update if there are vars to update
if len(toUpdate) > 0 {
fmt.Printf("Updating %d existing variables...\n", len(toUpdate))
bulkReq := &service.BulkUpdateEnvsRequest{
Data: toUpdate,
}
_, err := appSvc.BulkUpdateEnvs(ctx, uuid, bulkReq)
if err != nil {
fmt.Printf(" ✗ Bulk update failed: %v\n", err)
failCount += len(toUpdate)
} else {
updateCount = len(toUpdate)
fmt.Printf(" ✓ Successfully updated %d variables\n", updateCount)
}
}
// Create new variables one by one
if len(toCreate) > 0 {
fmt.Printf("Creating %d new variables...\n", len(toCreate))
for _, req := range toCreate {
_, err := appSvc.CreateEnv(ctx, uuid, &req)
if err != nil {
fmt.Printf(" ✗ Failed to create '%s': %v\n", req.Key, err)
failCount++
} else {
fmt.Printf(" ✓ Created '%s'\n", req.Key)
createCount++
}
}
}
fmt.Printf("\nSync complete: %d updated, %d created, %d failed\n", updateCount, createCount, failCount)
if failCount > 0 {
return fmt.Errorf("some environment variables failed to sync")
}
return nil
},
}
syncEnvCmd.Flags().StringP("file", "f", "", "Path to .env file (required)")
syncEnvCmd.Flags().Bool("build-time", false, "Make all variables available at build time")
syncEnvCmd.Flags().Bool("preview", false, "Make all variables available in preview deployments")
syncEnvCmd.Flags().Bool("is-literal", false, "Treat all values as literal (don't interpolate variables)")
return syncEnvCmd
}
+80
View File
@@ -0,0 +1,80 @@
package env
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewUpdateEnvCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <app_uuid> <env_uuid>",
Short: "Update an environment variable",
Long: `Update an existing environment variable. First UUID is the application, second is the specific environment variable to update.`,
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
envUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
req := &models.EnvironmentVariableUpdateRequest{
UUID: envUUID,
}
if cmd.Flags().Changed("key") {
key, _ := cmd.Flags().GetString("key")
req.Key = &key
}
if cmd.Flags().Changed("value") {
value, _ := cmd.Flags().GetString("value")
req.Value = &value
}
if cmd.Flags().Changed("build-time") {
isBuildTime, _ := cmd.Flags().GetBool("build-time")
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
isPreview, _ := cmd.Flags().GetBool("preview")
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
isLiteral, _ := cmd.Flags().GetBool("is-literal")
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("is-multiline") {
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
req.IsMultiline = &isMultiline
}
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsPreview == nil && req.IsLiteral == nil && req.IsMultiline == nil {
return fmt.Errorf("at least one field must be provided to update")
}
appSvc := service.NewApplicationService(client)
env, err := appSvc.UpdateEnv(ctx, appUUID, req)
if err != nil {
return fmt.Errorf("failed to update environment variable: %w", err)
}
fmt.Printf("Environment variable '%s' updated successfully.\n", env.Key)
return nil
},
}
cmd.Flags().String("key", "", "New environment variable key")
cmd.Flags().String("value", "", "New environment variable value")
cmd.Flags().Bool("build-time", false, "Available at build time")
cmd.Flags().Bool("preview", false, "Available in preview deployments")
cmd.Flags().Bool("is-literal", false, "Treat value as literal")
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
return cmd
}
+47
View File
@@ -0,0 +1,47 @@
package application
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <uuid>",
Short: "Get application details by UUID",
Long: `Retrieve detailed information about a specific application.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
app, err := appSvc.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get application: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(app)
},
}
}
+70
View File
@@ -0,0 +1,70 @@
package application
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all applications",
Long: `List all applications in Coolify.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
apps, err := appSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list applications: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// For JSON/pretty formats, return the full application structure
if format != output.FormatTable {
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(apps)
}
// For table format, convert to simplified rows
var rows []models.ApplicationListItem
for _, app := range apps {
rows = append(rows, models.ApplicationListItem{
UUID: app.UUID,
Name: app.Name,
Description: app.Description,
Status: app.Status,
GitBranch: app.GitBranch,
FQDN: app.FQDN,
})
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(rows)
},
}
}
+86
View File
@@ -0,0 +1,86 @@
package application
import (
"context"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewLogsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "logs <uuid>",
Short: "Get application logs",
Long: `Retrieve logs for an application. Use --follow to continuously stream new logs.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
lines, _ := cmd.Flags().GetInt("lines")
follow, _ := cmd.Flags().GetBool("follow")
appSvc := service.NewApplicationService(client)
if !follow {
resp, err := appSvc.Logs(ctx, uuid, lines)
if err != nil {
return fmt.Errorf("failed to get logs: %w", err)
}
fmt.Print(resp.Logs)
return nil
}
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
lastLogs := ""
resp, err := appSvc.Logs(ctx, uuid, lines)
if err != nil {
return fmt.Errorf("failed to get logs: %w", err)
}
fmt.Print(resp.Logs)
lastLogs = resp.Logs
for {
select {
case <-sigChan:
fmt.Println("\nStopping log follow...")
return nil
case <-ticker.C:
resp, err := appSvc.Logs(ctx, uuid, lines)
if err != nil {
continue
}
if resp.Logs != lastLogs {
if len(resp.Logs) > len(lastLogs) && strings.HasPrefix(resp.Logs, lastLogs) {
fmt.Print(resp.Logs[len(lastLogs):])
} else {
fmt.Print(resp.Logs)
}
lastLogs = resp.Logs
}
}
}
},
}
cmd.Flags().IntP("lines", "n", 100, "Number of log lines to retrieve")
cmd.Flags().BoolP("follow", "f", false, "Follow log output (like tail -f)")
return cmd
}
+37
View File
@@ -0,0 +1,37 @@
package application
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewRestartCommand() *cobra.Command {
return &cobra.Command{
Use: "restart <uuid>",
Short: "Restart an application",
Long: `Restart a running application.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
resp, err := appSvc.Restart(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to restart application: %w", err)
}
fmt.Println(resp.Message)
return nil
},
}
}
+48
View File
@@ -0,0 +1,48 @@
package application
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewStartCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "start <uuid>",
Aliases: []string{"deploy"},
Short: "Start an application",
Long: `Start an application (initiates a deployment).`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
instantDeploy, _ := cmd.Flags().GetBool("instant-deploy")
appSvc := service.NewApplicationService(client)
resp, err := appSvc.Start(ctx, uuid, force, instantDeploy)
if err != nil {
return fmt.Errorf("failed to start application: %w", err)
}
fmt.Println(resp.Message)
if resp.DeploymentUUID != nil && *resp.DeploymentUUID != "" {
fmt.Printf("Deployment UUID: %s\n", *resp.DeploymentUUID)
}
return nil
},
}
cmd.Flags().Bool("force", false, "Force rebuild")
cmd.Flags().Bool("instant-deploy", false, "Instant deploy (skip queuing)")
return cmd
}
+37
View File
@@ -0,0 +1,37 @@
package application
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewStopCommand() *cobra.Command {
return &cobra.Command{
Use: "stop <uuid>",
Short: "Stop an application",
Long: `Stop a running application.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
resp, err := appSvc.Stop(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to stop application: %w", err)
}
fmt.Println(resp.Message)
return nil
},
}
}
+161
View File
@@ -0,0 +1,161 @@
package application
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewUpdateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <uuid>",
Short: "Update application configuration",
Long: `Update configuration for a specific application. Only specified fields will be updated.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
req := models.ApplicationUpdateRequest{}
hasUpdates := false
if cmd.Flags().Changed("name") {
name, _ := cmd.Flags().GetString("name")
req.Name = &name
hasUpdates = true
}
if cmd.Flags().Changed("description") {
desc, _ := cmd.Flags().GetString("description")
req.Description = &desc
hasUpdates = true
}
if cmd.Flags().Changed("git-branch") {
branch, _ := cmd.Flags().GetString("git-branch")
req.GitBranch = &branch
hasUpdates = true
}
if cmd.Flags().Changed("git-repository") {
repo, _ := cmd.Flags().GetString("git-repository")
req.GitRepository = &repo
hasUpdates = true
}
if cmd.Flags().Changed("domains") {
domains, _ := cmd.Flags().GetString("domains")
req.Domains = &domains
hasUpdates = true
}
if cmd.Flags().Changed("build-command") {
buildCmd, _ := cmd.Flags().GetString("build-command")
req.BuildCommand = &buildCmd
hasUpdates = true
}
if cmd.Flags().Changed("start-command") {
startCmd, _ := cmd.Flags().GetString("start-command")
req.StartCommand = &startCmd
hasUpdates = true
}
if cmd.Flags().Changed("install-command") {
installCmd, _ := cmd.Flags().GetString("install-command")
req.InstallCommand = &installCmd
hasUpdates = true
}
if cmd.Flags().Changed("base-directory") {
baseDir, _ := cmd.Flags().GetString("base-directory")
req.BaseDirectory = &baseDir
hasUpdates = true
}
if cmd.Flags().Changed("publish-directory") {
publishDir, _ := cmd.Flags().GetString("publish-directory")
req.PublishDirectory = &publishDir
hasUpdates = true
}
if cmd.Flags().Changed("dockerfile") {
dockerfile, _ := cmd.Flags().GetString("dockerfile")
req.Dockerfile = &dockerfile
hasUpdates = true
}
if cmd.Flags().Changed("docker-image") {
image, _ := cmd.Flags().GetString("docker-image")
req.DockerRegistryImageName = &image
hasUpdates = true
}
if cmd.Flags().Changed("docker-tag") {
tag, _ := cmd.Flags().GetString("docker-tag")
req.DockerRegistryImageTag = &tag
hasUpdates = true
}
if cmd.Flags().Changed("ports-exposes") {
ports, _ := cmd.Flags().GetString("ports-exposes")
req.PortsExposes = &ports
hasUpdates = true
}
if cmd.Flags().Changed("ports-mappings") {
ports, _ := cmd.Flags().GetString("ports-mappings")
req.PortsMappings = &ports
hasUpdates = true
}
if cmd.Flags().Changed("health-check-enabled") {
enabled, _ := cmd.Flags().GetBool("health-check-enabled")
req.HealthCheckEnabled = &enabled
hasUpdates = true
}
if cmd.Flags().Changed("health-check-path") {
path, _ := cmd.Flags().GetString("health-check-path")
req.HealthCheckPath = &path
hasUpdates = true
}
if !hasUpdates {
return fmt.Errorf("no fields to update. Use --help to see available flags")
}
appSvc := service.NewApplicationService(client)
app, err := appSvc.Update(ctx, uuid, req)
if err != nil {
return fmt.Errorf("failed to update application: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(app)
},
}
cmd.Flags().String("name", "", "Application name")
cmd.Flags().String("description", "", "Application description")
cmd.Flags().String("git-branch", "", "Git branch")
cmd.Flags().String("git-repository", "", "Git repository URL")
cmd.Flags().String("domains", "", "Domains (comma-separated)")
cmd.Flags().String("build-command", "", "Build command")
cmd.Flags().String("start-command", "", "Start command")
cmd.Flags().String("install-command", "", "Install command")
cmd.Flags().String("base-directory", "", "Base directory")
cmd.Flags().String("publish-directory", "", "Publish directory")
cmd.Flags().String("dockerfile", "", "Dockerfile content")
cmd.Flags().String("docker-image", "", "Docker image name")
cmd.Flags().String("docker-tag", "", "Docker image tag")
cmd.Flags().String("ports-exposes", "", "Exposed ports")
cmd.Flags().String("ports-mappings", "", "Port mappings")
cmd.Flags().Bool("health-check-enabled", false, "Enable health check")
cmd.Flags().String("health-check-path", "", "Health check path")
return cmd
}
-910
View File
@@ -1,910 +0,0 @@
package cmd
import (
"context"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/parser"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
var applicationsCmd = &cobra.Command{
Use: "app",
Aliases: []string{"apps", "application", "applications"},
Short: "Application related commands",
Long: `Manage Coolify applications - list, get, create, update, delete, and control application lifecycle.`,
}
var listApplicationsCmd = &cobra.Command{
Use: "list",
Short: "List all applications",
Long: `List all applications in your Coolify instance.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
apps, err := appSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list applications: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// For JSON/pretty formats, return the full application structure
if format != output.FormatTable {
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(apps)
}
// For table format, convert to simplified rows
var rows []models.ApplicationListItem
for _, app := range apps {
rows = append(rows, models.ApplicationListItem{
UUID: app.UUID,
Name: app.Name,
Description: app.Description,
Status: app.Status,
GitBranch: app.GitBranch,
FQDN: app.FQDN,
})
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(rows)
},
}
var getApplicationCmd = &cobra.Command{
Use: "get <uuid>",
Short: "Get application details by UUID",
Long: `Retrieve detailed information about a specific application.`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
app, err := appSvc.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get application: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(app)
},
}
var updateApplicationCmd = &cobra.Command{
Use: "update <uuid>",
Short: "Update application configuration",
Long: `Update configuration for a specific application. Only specified fields will be updated.`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Build update request from flags
req := models.ApplicationUpdateRequest{}
hasUpdates := false
// Basic configuration
if cmd.Flags().Changed("name") {
name, _ := cmd.Flags().GetString("name")
req.Name = &name
hasUpdates = true
}
if cmd.Flags().Changed("description") {
desc, _ := cmd.Flags().GetString("description")
req.Description = &desc
hasUpdates = true
}
if cmd.Flags().Changed("git-branch") {
branch, _ := cmd.Flags().GetString("git-branch")
req.GitBranch = &branch
hasUpdates = true
}
if cmd.Flags().Changed("git-repository") {
repo, _ := cmd.Flags().GetString("git-repository")
req.GitRepository = &repo
hasUpdates = true
}
if cmd.Flags().Changed("domains") {
domains, _ := cmd.Flags().GetString("domains")
req.Domains = &domains
hasUpdates = true
}
// Build configuration
if cmd.Flags().Changed("build-command") {
buildCmd, _ := cmd.Flags().GetString("build-command")
req.BuildCommand = &buildCmd
hasUpdates = true
}
if cmd.Flags().Changed("start-command") {
startCmd, _ := cmd.Flags().GetString("start-command")
req.StartCommand = &startCmd
hasUpdates = true
}
if cmd.Flags().Changed("install-command") {
installCmd, _ := cmd.Flags().GetString("install-command")
req.InstallCommand = &installCmd
hasUpdates = true
}
if cmd.Flags().Changed("base-directory") {
baseDir, _ := cmd.Flags().GetString("base-directory")
req.BaseDirectory = &baseDir
hasUpdates = true
}
if cmd.Flags().Changed("publish-directory") {
publishDir, _ := cmd.Flags().GetString("publish-directory")
req.PublishDirectory = &publishDir
hasUpdates = true
}
// Docker configuration
if cmd.Flags().Changed("dockerfile") {
dockerfile, _ := cmd.Flags().GetString("dockerfile")
req.Dockerfile = &dockerfile
hasUpdates = true
}
if cmd.Flags().Changed("docker-image") {
image, _ := cmd.Flags().GetString("docker-image")
req.DockerRegistryImageName = &image
hasUpdates = true
}
if cmd.Flags().Changed("docker-tag") {
tag, _ := cmd.Flags().GetString("docker-tag")
req.DockerRegistryImageTag = &tag
hasUpdates = true
}
// Ports
if cmd.Flags().Changed("ports-exposes") {
ports, _ := cmd.Flags().GetString("ports-exposes")
req.PortsExposes = &ports
hasUpdates = true
}
if cmd.Flags().Changed("ports-mappings") {
ports, _ := cmd.Flags().GetString("ports-mappings")
req.PortsMappings = &ports
hasUpdates = true
}
// Health check
if cmd.Flags().Changed("health-check-enabled") {
enabled, _ := cmd.Flags().GetBool("health-check-enabled")
req.HealthCheckEnabled = &enabled
hasUpdates = true
}
if cmd.Flags().Changed("health-check-path") {
path, _ := cmd.Flags().GetString("health-check-path")
req.HealthCheckPath = &path
hasUpdates = true
}
if !hasUpdates {
return fmt.Errorf("no fields to update. Use --help to see available flags")
}
appSvc := service.NewApplicationService(client)
app, err := appSvc.Update(ctx, uuid, req)
if err != nil {
return fmt.Errorf("failed to update application: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(app)
},
}
var deleteApplicationCmd = &cobra.Command{
Use: "delete <uuid>",
Short: "Delete an application",
Long: `Delete an application. This action cannot be undone.`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
force, _ := cmd.Flags().GetBool("force")
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to delete application %s? This cannot be undone. (yes/no): ", uuid)
fmt.Scanln(&response)
if response != "yes" && response != "y" {
fmt.Println("Delete cancelled.")
return nil
}
}
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
err = appSvc.Delete(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to delete application: %w", err)
}
fmt.Printf("Application %s deleted successfully.\n", uuid)
return nil
},
}
func init() {
// Define update command flags (most common ones)
updateApplicationCmd.Flags().String("name", "", "Application name")
updateApplicationCmd.Flags().String("description", "", "Application description")
updateApplicationCmd.Flags().String("git-branch", "", "Git branch")
updateApplicationCmd.Flags().String("git-repository", "", "Git repository URL")
updateApplicationCmd.Flags().String("domains", "", "Domains (comma-separated)")
updateApplicationCmd.Flags().String("build-command", "", "Build command")
updateApplicationCmd.Flags().String("start-command", "", "Start command")
updateApplicationCmd.Flags().String("install-command", "", "Install command")
updateApplicationCmd.Flags().String("base-directory", "", "Base directory")
updateApplicationCmd.Flags().String("publish-directory", "", "Publish directory")
updateApplicationCmd.Flags().String("dockerfile", "", "Dockerfile content")
updateApplicationCmd.Flags().String("docker-image", "", "Docker image name")
updateApplicationCmd.Flags().String("docker-tag", "", "Docker image tag")
updateApplicationCmd.Flags().String("ports-exposes", "", "Exposed ports")
updateApplicationCmd.Flags().String("ports-mappings", "", "Port mappings")
updateApplicationCmd.Flags().Bool("health-check-enabled", false, "Enable health check")
updateApplicationCmd.Flags().String("health-check-path", "", "Health check path")
// Define delete command flags
deleteApplicationCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
// Define start command flags
startApplicationCmd.Flags().Bool("force", false, "Force rebuild")
startApplicationCmd.Flags().Bool("instant-deploy", false, "Instant deploy (skip queuing)")
// Define logs command flags
logsApplicationCmd.Flags().IntP("lines", "n", 100, "Number of log lines to retrieve")
logsApplicationCmd.Flags().BoolP("follow", "f", false, "Follow log output (like tail -f)")
// Define envs create command flags
createEnvCmd.Flags().String("key", "", "Environment variable key (required)")
createEnvCmd.Flags().String("value", "", "Environment variable value (required)")
createEnvCmd.Flags().Bool("build-time", false, "Available at build time")
createEnvCmd.Flags().Bool("preview", false, "Available in preview deployments")
createEnvCmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
createEnvCmd.Flags().Bool("is-multiline", false, "Value is multiline")
// Define envs update command flags
updateEnvCmd.Flags().String("key", "", "New environment variable key")
updateEnvCmd.Flags().String("value", "", "New environment variable value")
updateEnvCmd.Flags().Bool("build-time", false, "Available at build time")
updateEnvCmd.Flags().Bool("preview", false, "Available in preview deployments")
updateEnvCmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
updateEnvCmd.Flags().Bool("is-multiline", false, "Value is multiline")
// Define envs delete command flags
deleteEnvCmd.Flags().Bool("force", false, "Skip confirmation prompt")
// Define envs sync command flags
syncEnvCmd.Flags().StringP("file", "f", "", "Path to .env file (required)")
syncEnvCmd.Flags().Bool("build-time", false, "Make all variables available at build time")
syncEnvCmd.Flags().Bool("preview", false, "Make all variables available in preview deployments")
syncEnvCmd.Flags().Bool("is-literal", false, "Treat all values as literal (don't interpolate variables)")
rootCmd.AddCommand(applicationsCmd)
applicationsCmd.AddCommand(listApplicationsCmd)
applicationsCmd.AddCommand(getApplicationCmd)
applicationsCmd.AddCommand(updateApplicationCmd)
applicationsCmd.AddCommand(deleteApplicationCmd)
applicationsCmd.AddCommand(startApplicationCmd)
applicationsCmd.AddCommand(stopApplicationCmd)
applicationsCmd.AddCommand(restartApplicationCmd)
applicationsCmd.AddCommand(logsApplicationCmd)
applicationsCmd.AddCommand(envsApplicationCmd)
envsApplicationCmd.AddCommand(listEnvsCmd)
envsApplicationCmd.AddCommand(getEnvCmd)
envsApplicationCmd.AddCommand(createEnvCmd)
envsApplicationCmd.AddCommand(updateEnvCmd)
envsApplicationCmd.AddCommand(deleteEnvCmd)
envsApplicationCmd.AddCommand(syncEnvCmd)
}
var startApplicationCmd = &cobra.Command{
Use: "start <uuid>",
Aliases: []string{"deploy"},
Short: "Start an application",
Long: `Start an application (initiates a deployment).`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
instantDeploy, _ := cmd.Flags().GetBool("instant-deploy")
appSvc := service.NewApplicationService(client)
resp, err := appSvc.Start(ctx, uuid, force, instantDeploy)
if err != nil {
return fmt.Errorf("failed to start application: %w", err)
}
fmt.Println(resp.Message)
if resp.DeploymentUUID != nil && *resp.DeploymentUUID != "" {
fmt.Printf("Deployment UUID: %s\n", *resp.DeploymentUUID)
}
return nil
},
}
var stopApplicationCmd = &cobra.Command{
Use: "stop <uuid>",
Short: "Stop an application",
Long: `Stop a running application.`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
resp, err := appSvc.Stop(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to stop application: %w", err)
}
fmt.Println(resp.Message)
return nil
},
}
var restartApplicationCmd = &cobra.Command{
Use: "restart <uuid>",
Short: "Restart an application",
Long: `Restart a running application.`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
resp, err := appSvc.Restart(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to restart application: %w", err)
}
fmt.Println(resp.Message)
return nil
},
}
var logsApplicationCmd = &cobra.Command{
Use: "logs <uuid>",
Short: "Get application logs",
Long: `Retrieve logs for an application. Use --follow to continuously stream new logs.`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
lines, _ := cmd.Flags().GetInt("lines")
follow, _ := cmd.Flags().GetBool("follow")
appSvc := service.NewApplicationService(client)
if !follow {
// One-time fetch
resp, err := appSvc.Logs(ctx, uuid, lines)
if err != nil {
return fmt.Errorf("failed to get logs: %w", err)
}
fmt.Print(resp.Logs)
return nil
}
// Follow mode: poll for new logs
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
// Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
// Track the last log content to avoid duplicates
lastLogs := ""
// Fetch initial logs
resp, err := appSvc.Logs(ctx, uuid, lines)
if err != nil {
return fmt.Errorf("failed to get logs: %w", err)
}
fmt.Print(resp.Logs)
lastLogs = resp.Logs
// Poll for new logs
for {
select {
case <-sigChan:
fmt.Println("\nStopping log follow...")
return nil
case <-ticker.C:
resp, err := appSvc.Logs(ctx, uuid, lines)
if err != nil {
// Don't fail on transient errors in follow mode
continue
}
// Only print if logs have changed
if resp.Logs != lastLogs {
// Print only the new content
if len(resp.Logs) > len(lastLogs) && strings.HasPrefix(resp.Logs, lastLogs) {
fmt.Print(resp.Logs[len(lastLogs):])
} else {
// Logs were truncated or changed, print all
fmt.Print(resp.Logs)
}
lastLogs = resp.Logs
}
}
}
},
}
var envsApplicationCmd = &cobra.Command{
Use: "env",
Aliases: []string{"envs", "environment"},
Short: "Manage application environment variables",
Long: `List and manage environment variables for applications. All commands require the application UUID first to establish context.`,
}
var listEnvsCmd = &cobra.Command{
Use: "list <app_uuid>",
Short: "List all environment variables for an application",
Long: `List all environment variables for a specific application.`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
envs, err := appSvc.ListEnvs(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to list environment variables: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// Mask sensitive values unless --show-sensitive is used
if !showSensitive {
for i := range envs {
envs[i].Value = "********"
if envs[i].RealValue != nil {
masked := "********"
envs[i].RealValue = &masked
}
}
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(envs)
},
}
var getEnvCmd = &cobra.Command{
Use: "get <app_uuid> <env_uuid_or_key>",
Short: "Get environment variable details",
Long: `Get detailed information about a specific environment variable by UUID or key name.`,
Args: exactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
envUUID := args[1]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
env, err := appSvc.GetEnv(ctx, appUUID, envUUID)
if err != nil {
return fmt.Errorf("failed to get environment variable: %w", err)
}
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// Mask sensitive value unless --show-sensitive is used
if !showSensitive {
env.Value = "********"
if env.RealValue != nil {
masked := "********"
env.RealValue = &masked
}
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(env)
},
}
var createEnvCmd = &cobra.Command{
Use: "create <app_uuid>",
Short: "Create an environment variable for an application",
Long: `Create a new environment variable for a specific application. Use --key and --value flags to specify the variable.`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
key, _ := cmd.Flags().GetString("key")
value, _ := cmd.Flags().GetString("value")
isBuildTime, _ := cmd.Flags().GetBool("build-time")
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
if key == "" {
return fmt.Errorf("--key is required")
}
if value == "" {
return fmt.Errorf("--value is required")
}
req := &models.EnvironmentVariableCreateRequest{
Key: key,
Value: value,
}
// Only set flags if they were explicitly provided
if cmd.Flags().Changed("build-time") {
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("is-multiline") {
req.IsMultiline = &isMultiline
}
appSvc := service.NewApplicationService(client)
env, err := appSvc.CreateEnv(ctx, uuid, req)
if err != nil {
return fmt.Errorf("failed to create environment variable: %w", err)
}
fmt.Printf("Environment variable '%s' created successfully.\n", env.Key)
fmt.Printf("UUID: %s\n", env.UUID)
return nil
},
}
var updateEnvCmd = &cobra.Command{
Use: "update <app_uuid> <env_uuid>",
Short: "Update an environment variable",
Long: `Update an existing environment variable. First UUID is the application, second is the specific environment variable to update.`,
Args: exactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error{
ctx := context.Background()
appUUID := args[0]
envUUID := args[1]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
req := &models.EnvironmentVariableUpdateRequest{
UUID: envUUID,
}
// Only set fields that were provided
if cmd.Flags().Changed("key") {
key, _ := cmd.Flags().GetString("key")
req.Key = &key
}
if cmd.Flags().Changed("value") {
value, _ := cmd.Flags().GetString("value")
req.Value = &value
}
if cmd.Flags().Changed("build-time") {
isBuildTime, _ := cmd.Flags().GetBool("build-time")
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
isPreview, _ := cmd.Flags().GetBool("preview")
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
isLiteral, _ := cmd.Flags().GetBool("is-literal")
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("is-multiline") {
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
req.IsMultiline = &isMultiline
}
// Check if at least one field is being updated
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsPreview == nil && req.IsLiteral == nil && req.IsMultiline == nil {
return fmt.Errorf("at least one field must be provided to update (--key, --value, --build-time, --preview, --is-literal, or --is-multiline)")
}
appSvc := service.NewApplicationService(client)
env, err := appSvc.UpdateEnv(ctx, appUUID, req)
if err != nil {
return fmt.Errorf("failed to update environment variable: %w", err)
}
fmt.Printf("Environment variable '%s' updated successfully.\n", env.Key)
return nil
},
}
var deleteEnvCmd = &cobra.Command{
Use: "delete <app_uuid> <env_uuid>",
Short: "Delete an environment variable",
Long: `Delete an environment variable from an application. First UUID is the application, second is the specific environment variable to delete.`,
Args: exactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
envUUID := args[1]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to delete this environment variable? (yes/no): ")
fmt.Scanln(&response)
if response != "yes" && response != "y" {
fmt.Println("Delete cancelled.")
return nil
}
}
appSvc := service.NewApplicationService(client)
err = appSvc.DeleteEnv(ctx, appUUID, envUUID)
if err != nil {
return fmt.Errorf("failed to delete environment variable: %w", err)
}
fmt.Println("Environment variable deleted successfully.")
return nil
},
}
var syncEnvCmd = &cobra.Command{
Use: "sync <app_uuid>",
Short: "Sync environment variables from a .env file",
Long: `Sync environment variables from a .env file. This command intelligently:
- Updates existing environment variables with new values
- Creates new environment variables that don't exist yet
- Uses efficient bulk operations where possible
Example: coolify app env sync abc123 --file .env.production`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
filePath, _ := cmd.Flags().GetString("file")
if filePath == "" {
return fmt.Errorf("--file is required")
}
isBuildTime, _ := cmd.Flags().GetBool("build-time")
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
// Parse the .env file
envVars, err := parser.ParseEnvFile(filePath)
if err != nil {
return fmt.Errorf("failed to parse .env file: %w", err)
}
if len(envVars) == 0 {
fmt.Println("No environment variables found in file.")
return nil
}
fmt.Printf("Found %d environment variables in file. Syncing...\n", len(envVars))
// Fetch existing environment variables
appSvc := service.NewApplicationService(client)
existingEnvs, err := appSvc.ListEnvs(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to list existing environment variables: %w", err)
}
// Build a map of existing env vars by key
existingMap := make(map[string]models.EnvironmentVariable)
for _, env := range existingEnvs {
existingMap[env.Key] = env
}
// Separate into updates and creates
var toUpdate []models.EnvironmentVariableCreateRequest
var toCreate []models.EnvironmentVariableCreateRequest
for _, envVar := range envVars {
req := models.EnvironmentVariableCreateRequest{
Key: envVar.Key,
Value: envVar.Value,
}
// Apply flags if explicitly provided
if cmd.Flags().Changed("build-time") {
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
req.IsLiteral = &isLiteral
}
// Auto-detect multiline values
if strings.Contains(envVar.Value, "\n") {
multiline := true
req.IsMultiline = &multiline
}
if _, exists := existingMap[envVar.Key]; exists {
toUpdate = append(toUpdate, req)
} else {
toCreate = append(toCreate, req)
}
}
updateCount := 0
createCount := 0
failCount := 0
// Perform bulk update if there are vars to update
if len(toUpdate) > 0 {
fmt.Printf("Updating %d existing variables...\n", len(toUpdate))
bulkReq := &service.BulkUpdateEnvsRequest{
Data: toUpdate,
}
_, err := appSvc.BulkUpdateEnvs(ctx, uuid, bulkReq)
if err != nil {
fmt.Printf(" ✗ Bulk update failed: %v\n", err)
failCount += len(toUpdate)
} else {
updateCount = len(toUpdate)
fmt.Printf(" ✓ Successfully updated %d variables\n", updateCount)
}
}
// Create new variables one by one
if len(toCreate) > 0 {
fmt.Printf("Creating %d new variables...\n", len(toCreate))
for _, req := range toCreate {
_, err := appSvc.CreateEnv(ctx, uuid, &req)
if err != nil {
fmt.Printf(" ✗ Failed to create '%s': %v\n", req.Key, err)
failCount++
} else {
fmt.Printf(" ✓ Created '%s'\n", req.Key)
createCount++
}
}
}
fmt.Printf("\nSync complete: %d updated, %d created, %d failed\n", updateCount, createCount, failCount)
if failCount > 0 {
return fmt.Errorf("some environment variables failed to sync")
}
return nil
},
}
-466
View File
@@ -1,466 +0,0 @@
package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestApplicationsListCmd_Flags(t *testing.T) {
cmd := listApplicationsCmd
// Verify command structure
assert.Equal(t, "list", cmd.Use)
assert.NotNil(t, cmd.RunE)
}
func TestApplicationsGetCmd_Args(t *testing.T) {
cmd := getApplicationCmd
// Test with no arguments - should fail
err := cmd.Args(cmd, []string{})
assert.Error(t, err, "should require exactly 1 argument")
// Test with correct number of arguments - should pass
err = cmd.Args(cmd, []string{"app-uuid-123"})
assert.NoError(t, err, "should accept 1 argument")
// Test with too many arguments - should fail
err = cmd.Args(cmd, []string{"uuid1", "uuid2"})
assert.Error(t, err, "should not accept more than 1 argument")
}
func TestApplicationsGetCmd_Flags(t *testing.T) {
cmd := getApplicationCmd
// Verify command structure
assert.Equal(t, "get <uuid>", cmd.Use)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Args)
}
func TestApplicationsUpdateCmd_Args(t *testing.T) {
cmd := updateApplicationCmd
// Test with no arguments - should fail
err := cmd.Args(cmd, []string{})
assert.Error(t, err, "should require exactly 1 argument")
// Test with correct number of arguments - should pass
err = cmd.Args(cmd, []string{"app-uuid-123"})
assert.NoError(t, err, "should accept 1 argument")
// Test with too many arguments - should fail
err = cmd.Args(cmd, []string{"uuid1", "uuid2"})
assert.Error(t, err, "should not accept more than 1 argument")
}
func TestApplicationsUpdateCmd_Flags(t *testing.T) {
cmd := updateApplicationCmd
// Verify command structure
assert.Equal(t, "update <uuid>", cmd.Use)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Args)
// Verify key flags exist
assert.NotNil(t, cmd.Flags().Lookup("name"))
assert.NotNil(t, cmd.Flags().Lookup("description"))
assert.NotNil(t, cmd.Flags().Lookup("git-branch"))
assert.NotNil(t, cmd.Flags().Lookup("domains"))
assert.NotNil(t, cmd.Flags().Lookup("build-command"))
assert.NotNil(t, cmd.Flags().Lookup("start-command"))
assert.NotNil(t, cmd.Flags().Lookup("docker-image"))
assert.NotNil(t, cmd.Flags().Lookup("health-check-enabled"))
}
func TestApplicationsCmd_Structure(t *testing.T) {
// Verify parent command exists
assert.Equal(t, "applications", applicationsCmd.Use)
assert.NotEmpty(t, applicationsCmd.Short)
// Verify subcommands are registered
hasListCmd := false
hasGetCmd := false
hasUpdateCmd := false
for _, cmd := range applicationsCmd.Commands() {
if cmd.Use == "list" {
hasListCmd = true
}
if cmd.Use == "get <uuid>" {
hasGetCmd = true
}
if cmd.Use == "update <uuid>" {
hasUpdateCmd = true
}
}
assert.True(t, hasListCmd, "list subcommand should be registered")
assert.True(t, hasGetCmd, "get subcommand should be registered")
assert.True(t, hasUpdateCmd, "update subcommand should be registered")
}
func TestApplicationsDeleteCmd_Args(t *testing.T) {
cmd := deleteApplicationCmd
// Test with no arguments - should fail
err := cmd.Args(cmd, []string{})
assert.Error(t, err, "should require exactly 1 argument")
// Test with correct number of arguments - should pass
err = cmd.Args(cmd, []string{"app-uuid-123"})
assert.NoError(t, err, "should accept 1 argument")
// Test with too many arguments - should fail
err = cmd.Args(cmd, []string{"uuid1", "uuid2"})
assert.Error(t, err, "should not accept more than 1 argument")
}
func TestApplicationsDeleteCmd_Flags(t *testing.T) {
cmd := deleteApplicationCmd
// Verify command structure
assert.Equal(t, "delete <uuid>", cmd.Use)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Args)
// Verify force flag exists
forceFlag := cmd.Flags().Lookup("force")
assert.NotNil(t, forceFlag)
assert.Equal(t, "false", forceFlag.DefValue)
}
func TestApplicationsCmd_AllSubcommands(t *testing.T) {
// Verify all subcommands are registered
hasListCmd := false
hasGetCmd := false
hasUpdateCmd := false
hasDeleteCmd := false
for _, cmd := range applicationsCmd.Commands() {
switch cmd.Use {
case "list":
hasListCmd = true
case "get <uuid>":
hasGetCmd = true
case "update <uuid>":
hasUpdateCmd = true
case "delete <uuid>":
hasDeleteCmd = true
}
}
assert.True(t, hasListCmd, "list subcommand should be registered")
assert.True(t, hasGetCmd, "get subcommand should be registered")
assert.True(t, hasUpdateCmd, "update subcommand should be registered")
assert.True(t, hasDeleteCmd, "delete subcommand should be registered")
}
func TestApplicationsStartCmd_Args(t *testing.T) {
cmd := startApplicationCmd
// Test with no arguments - should fail
err := cmd.Args(cmd, []string{})
assert.Error(t, err, "should require exactly 1 argument")
// Test with correct number of arguments - should pass
err = cmd.Args(cmd, []string{"app-uuid-123"})
assert.NoError(t, err, "should accept 1 argument")
// Test with too many arguments - should fail
err = cmd.Args(cmd, []string{"uuid1", "uuid2"})
assert.Error(t, err, "should not accept more than 1 argument")
}
func TestApplicationsStartCmd_Structure(t *testing.T) {
cmd := startApplicationCmd
assert.Equal(t, "start <uuid>", cmd.Use)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Args)
// Verify aliases exist
assert.Contains(t, cmd.Aliases, "deploy")
// Verify flags exist
assert.NotNil(t, cmd.Flags().Lookup("force"))
assert.NotNil(t, cmd.Flags().Lookup("instant-deploy"))
}
func TestApplicationsStopCmd_Args(t *testing.T) {
cmd := stopApplicationCmd
// Test with no arguments - should fail
err := cmd.Args(cmd, []string{})
assert.Error(t, err, "should require exactly 1 argument")
// Test with correct number of arguments - should pass
err = cmd.Args(cmd, []string{"app-uuid-123"})
assert.NoError(t, err, "should accept 1 argument")
// Test with too many arguments - should fail
err = cmd.Args(cmd, []string{"uuid1", "uuid2"})
assert.Error(t, err, "should not accept more than 1 argument")
}
func TestApplicationsStopCmd_Structure(t *testing.T) {
cmd := stopApplicationCmd
assert.Equal(t, "stop <uuid>", cmd.Use)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Args)
}
func TestApplicationsRestartCmd_Args(t *testing.T) {
cmd := restartApplicationCmd
// Test with no arguments - should fail
err := cmd.Args(cmd, []string{})
assert.Error(t, err, "should require exactly 1 argument")
// Test with correct number of arguments - should pass
err = cmd.Args(cmd, []string{"app-uuid-123"})
assert.NoError(t, err, "should accept 1 argument")
// Test with too many arguments - should fail
err = cmd.Args(cmd, []string{"uuid1", "uuid2"})
assert.Error(t, err, "should not accept more than 1 argument")
}
func TestApplicationsRestartCmd_Structure(t *testing.T) {
cmd := restartApplicationCmd
assert.Equal(t, "restart <uuid>", cmd.Use)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Args)
}
func TestApplicationsLogsCmd_Args(t *testing.T) {
cmd := logsApplicationCmd
// Test with no arguments - should fail
err := cmd.Args(cmd, []string{})
assert.Error(t, err, "should require exactly 1 argument")
// Test with correct number of arguments - should pass
err = cmd.Args(cmd, []string{"app-uuid-123"})
assert.NoError(t, err, "should accept 1 argument")
// Test with too many arguments - should fail
err = cmd.Args(cmd, []string{"uuid1", "uuid2"})
assert.Error(t, err, "should not accept more than 1 argument")
}
func TestApplicationsLogsCmd_Structure(t *testing.T) {
cmd := logsApplicationCmd
assert.Equal(t, "logs <uuid>", cmd.Use)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Args)
// Verify flags exist
assert.NotNil(t, cmd.Flags().Lookup("lines"))
assert.NotNil(t, cmd.Flags().Lookup("follow"))
}
func TestApplicationsCmd_AllLifecycleCommands(t *testing.T) {
// Verify all lifecycle subcommands are registered
hasStartCmd := false
hasStopCmd := false
hasRestartCmd := false
hasLogsCmd := false
for _, cmd := range applicationsCmd.Commands() {
switch cmd.Use {
case "start <uuid>":
hasStartCmd = true
case "stop <uuid>":
hasStopCmd = true
case "restart <uuid>":
hasRestartCmd = true
case "logs <uuid>":
hasLogsCmd = true
}
}
assert.True(t, hasStartCmd, "start subcommand should be registered")
assert.True(t, hasStopCmd, "stop subcommand should be registered")
assert.True(t, hasRestartCmd, "restart subcommand should be registered")
assert.True(t, hasLogsCmd, "logs subcommand should be registered")
}
func TestApplicationsEnvsCmd_Structure(t *testing.T) {
cmd := envsApplicationCmd
assert.Equal(t, "envs", cmd.Use)
assert.NotNil(t, cmd.Commands())
assert.Greater(t, len(cmd.Commands()), 0, "envs should have subcommands")
}
func TestApplicationsEnvsListCmd_Args(t *testing.T) {
cmd := listEnvsCmd
// Test with no arguments - should fail
err := cmd.Args(cmd, []string{})
assert.Error(t, err, "should require exactly 1 argument")
// Test with correct number of arguments - should pass
err = cmd.Args(cmd, []string{"app-uuid-123"})
assert.NoError(t, err, "should accept 1 argument")
// Test with too many arguments - should fail
err = cmd.Args(cmd, []string{"uuid1", "uuid2"})
assert.Error(t, err, "should not accept more than 1 argument")
}
func TestApplicationsEnvsListCmd_Structure(t *testing.T) {
cmd := listEnvsCmd
assert.Equal(t, "list <uuid>", cmd.Use)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Args)
}
func TestApplicationsCmd_HasEnvsSubcommand(t *testing.T) {
// Verify envs subcommand is registered
hasEnvsCmd := false
for _, cmd := range applicationsCmd.Commands() {
if cmd.Use == "envs" {
hasEnvsCmd = true
break
}
}
assert.True(t, hasEnvsCmd, "envs subcommand should be registered")
}
func TestApplicationsEnvsCreateCmd_Args(t *testing.T) {
cmd := createEnvCmd
// Test with no arguments - should fail
err := cmd.Args(cmd, []string{})
assert.Error(t, err, "should require exactly 1 argument")
// Test with correct number of arguments - should pass
err = cmd.Args(cmd, []string{"app-uuid-123"})
assert.NoError(t, err, "should accept 1 argument")
// Test with too many arguments - should fail
err = cmd.Args(cmd, []string{"uuid1", "uuid2"})
assert.Error(t, err, "should not accept more than 1 argument")
}
func TestApplicationsEnvsCreateCmd_Structure(t *testing.T) {
cmd := createEnvCmd
assert.Equal(t, "create <uuid>", cmd.Use)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Args)
// Verify flags exist
assert.NotNil(t, cmd.Flags().Lookup("key"))
assert.NotNil(t, cmd.Flags().Lookup("value"))
assert.NotNil(t, cmd.Flags().Lookup("build-time"))
assert.NotNil(t, cmd.Flags().Lookup("preview"))
assert.NotNil(t, cmd.Flags().Lookup("is-literal"))
assert.NotNil(t, cmd.Flags().Lookup("is-multiline"))
}
func TestApplicationsEnvsUpdateCmd_Args(t *testing.T) {
cmd := updateEnvCmd
// Test with no arguments - should fail
err := cmd.Args(cmd, []string{})
assert.Error(t, err, "should require exactly 2 arguments")
// Test with 1 argument - should fail
err = cmd.Args(cmd, []string{"app-uuid-123"})
assert.Error(t, err, "should require exactly 2 arguments")
// Test with correct number of arguments - should pass
err = cmd.Args(cmd, []string{"app-uuid-123", "env-uuid-456"})
assert.NoError(t, err, "should accept 2 arguments")
// Test with too many arguments - should fail
err = cmd.Args(cmd, []string{"uuid1", "uuid2", "uuid3"})
assert.Error(t, err, "should not accept more than 2 arguments")
}
func TestApplicationsEnvsUpdateCmd_Structure(t *testing.T) {
cmd := updateEnvCmd
assert.Equal(t, "update <app_uuid> <env_uuid>", cmd.Use)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Args)
// Verify flags exist
assert.NotNil(t, cmd.Flags().Lookup("key"))
assert.NotNil(t, cmd.Flags().Lookup("value"))
assert.NotNil(t, cmd.Flags().Lookup("build-time"))
assert.NotNil(t, cmd.Flags().Lookup("preview"))
assert.NotNil(t, cmd.Flags().Lookup("is-literal"))
assert.NotNil(t, cmd.Flags().Lookup("is-multiline"))
}
func TestApplicationsEnvsDeleteCmd_Args(t *testing.T) {
cmd := deleteEnvCmd
// Test with no arguments - should fail
err := cmd.Args(cmd, []string{})
assert.Error(t, err, "should require exactly 2 arguments")
// Test with 1 argument - should fail
err = cmd.Args(cmd, []string{"app-uuid-123"})
assert.Error(t, err, "should require exactly 2 arguments")
// Test with correct number of arguments - should pass
err = cmd.Args(cmd, []string{"app-uuid-123", "env-uuid-456"})
assert.NoError(t, err, "should accept 2 arguments")
// Test with too many arguments - should fail
err = cmd.Args(cmd, []string{"uuid1", "uuid2", "uuid3"})
assert.Error(t, err, "should not accept more than 2 arguments")
}
func TestApplicationsEnvsDeleteCmd_Structure(t *testing.T) {
cmd := deleteEnvCmd
assert.Equal(t, "delete <app_uuid> <env_uuid>", cmd.Use)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Args)
// Verify flags exist
assert.NotNil(t, cmd.Flags().Lookup("force"))
}
func TestApplicationsEnvsImportCmd_Args(t *testing.T) {
cmd := importEnvCmd
// Test with no arguments - should fail
err := cmd.Args(cmd, []string{})
assert.Error(t, err, "should require exactly 1 argument")
// Test with correct number of arguments - should pass
err = cmd.Args(cmd, []string{"app-uuid-123"})
assert.NoError(t, err, "should accept 1 argument")
// Test with too many arguments - should fail
err = cmd.Args(cmd, []string{"uuid1", "uuid2"})
assert.Error(t, err, "should not accept more than 1 argument")
}
func TestApplicationsEnvsImportCmd_Structure(t *testing.T) {
cmd := importEnvCmd
assert.Equal(t, "import <uuid>", cmd.Use)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Args)
// Verify flags exist
assert.NotNil(t, cmd.Flags().Lookup("file"))
assert.NotNil(t, cmd.Flags().Lookup("build-time"))
assert.NotNil(t, cmd.Flags().Lookup("preview"))
assert.NotNil(t, cmd.Flags().Lookup("is-literal"))
}
+96
View File
@@ -0,0 +1,96 @@
package completion
import (
"fmt"
"os"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
)
func NewCompletionsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "completion <shell>",
Short: "Output shell completion code for the specified shell",
Long: `To load completions:
### Bash
To load completions into the current shell execute:
source <(coolify completion bash)
In order to make the completions permanent, append the line above to
your .bashrc.
### Zsh
If shell completions are not already enabled for your environment need
to enable them. Add the following line to your ~/.zshrc file:
autoload -Uz compinit; compinit
To load completions for each session execute the following commands:
mkdir -p ~/.config/coolify/completion/zsh
coolify completion zsh > ~/.config/coolify/completion/zsh/_coolify
Finally add the following line to your ~/.zshrc file, *before* you
call the compinit function:
fpath+=(~/.config/coolify/completion/zsh)
In the end your ~/.zshrc file should contain the following two lines
in the order given here.
fpath+=(~/.config/coolify/completion/zsh)
# ... anything else that needs to be done before compinit
autoload -Uz compinit; compinit
# ...
You will need to start a new shell for this setup to take effect.
### Fish
To load completions into the current shell execute:
coolify completion fish | source
In order to make the completions permanent execute once:
coolify completion fish > ~/.config/fish/completions/coolify.fish
### PowerShell:
To load completions into the current shell execute:
PS> coolify completion powershell | Out-String | Invoke-Expression
To load completions for every new session, run
and source this file from your PowerShell profile.
PS> coolify completion powershell > coolify.ps1
`,
Args: cli.ExactArgs(1, "<shell>"),
ValidArgs: []string{"bash", "fish", "zsh", "powershell"},
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) error {
var err error
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletion(os.Stdout)
case "fish":
err = cmd.Root().GenFishCompletion(os.Stdout, true)
case "zsh":
err = cmd.Root().GenZshCompletion(os.Stdout)
case "powershell":
err = cmd.Root().GenPowerShellCompletion(os.Stdout)
default:
err = fmt.Errorf("Unsupported shell: %s", args[0])
}
return err
},
}
return cmd
}
+20
View File
@@ -0,0 +1,20 @@
package config
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/config"
"github.com/spf13/cobra"
)
// NewConfigCommand creates the config command
func NewConfigCommand() *cobra.Command {
return &cobra.Command{
Use: "config",
Short: "Show configuration file location",
Long: "Display the path to the Coolify CLI configuration file",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(config.Path())
},
}
}
+52
View File
@@ -0,0 +1,52 @@
package config
import (
"strings"
"testing"
"github.com/coollabsio/coolify-cli/internal/config"
)
func TestNewConfigCommand(t *testing.T) {
cmd := NewConfigCommand()
if cmd == nil {
t.Fatal("NewConfigCommand() returned nil")
}
if cmd.Use != "config" {
t.Errorf("Expected Use to be 'config', got '%s'", cmd.Use)
}
if cmd.Short == "" {
t.Error("Short description should not be empty")
}
if cmd.Long == "" {
t.Error("Long description should not be empty")
}
if cmd.Run == nil {
t.Error("Run function should not be nil")
}
}
func TestConfigCommand_Output(t *testing.T) {
// Test that the command returns the expected config path
expectedPath := config.Path()
// The path should not be empty
if expectedPath == "" {
t.Error("Expected config path to not be empty")
}
// The path should end with config.json
if !strings.HasSuffix(expectedPath, "config.json") {
t.Errorf("Expected path to end with 'config.json', got '%s'", expectedPath)
}
// The path should contain the coolify directory
if !strings.Contains(expectedPath, "coolify") {
t.Errorf("Expected path to contain 'coolify', got '%s'", expectedPath)
}
}
+89
View File
@@ -0,0 +1,89 @@
package context
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// NewAddCommand creates the add command
func NewAddCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "add <context_name> <url> <token>",
Example: `context add myserver https://coolify.example.com your-api-token`,
Args: cli.ExactArgs(3, "<context_name> <url> <token>"),
Short: "Add a new context",
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
host := args[1]
token := args[2]
force, _ := cmd.Flags().GetBool("force")
setDefault, _ := cmd.Flags().GetBool("default")
instances := viper.Get("instances").([]any)
// Check if instance already exists
for _, instance := range instances {
instanceMap := instance.(map[string]any)
if instanceMap["name"] == name {
if force {
instanceMap["token"] = token
if setDefault {
// Remove default from all instances
for _, inst := range instances {
instMap := inst.(map[string]any)
instMap["default"] = false
}
instanceMap["default"] = true
fmt.Printf("%s already exists. Force overwriting. Setting it as default.\n", name)
} else {
fmt.Printf("%s already exists. Force overwriting.\n", name)
}
viper.Set("instances", instances)
viper.WriteConfig()
return
}
fmt.Printf("%s already exists.\n", name)
fmt.Println("\nNote: Use --force to force overwrite.")
return
}
}
// Add new instance
newInstance := config.Instance{
Name: name,
FQDN: host,
Token: token,
Default: false,
}
if setDefault {
// Remove default from all instances
for _, inst := range instances {
instMap := inst.(map[string]any)
instMap["default"] = false
}
newInstance.Default = true
fmt.Printf("Context '%s' added and set as default.\n", newInstance.Name)
} else {
fmt.Printf("Context '%s' added successfully.\n", newInstance.Name)
}
instances = append(instances, newInstance)
viper.Set("instances", instances)
if err := viper.WriteConfig(); err != nil {
fmt.Printf("failed to write config: %v\n", err)
}
},
}
cmd.Flags().BoolP("default", "d", false, "Set as default context")
cmd.Flags().BoolP("force", "f", false, "Force overwrite if context already exists")
return cmd
}
+28
View File
@@ -0,0 +1,28 @@
package context
import (
"github.com/spf13/cobra"
)
// NewContextCommand creates the context parent command
func NewContextCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "context",
Short: "Manage Coolify contexts",
Long: `Manage Coolify contexts. A context contains the configuration (URL and token) for connecting to Coolify.`,
}
// Add subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewAddCommand())
cmd.AddCommand(NewDeleteCommand())
cmd.AddCommand(NewUseCommand())
cmd.AddCommand(NewUpdateCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewSetTokenCommand())
cmd.AddCommand(NewSetDefaultCommand())
cmd.AddCommand(NewVersionCommand())
cmd.AddCommand(NewVerifyCommand())
return cmd
}
+49
View File
@@ -0,0 +1,49 @@
package context
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"slices"
)
// NewDeleteCommand creates the delete command
func NewDeleteCommand() *cobra.Command {
return &cobra.Command{
Use: "delete <context_name>",
Example: `context delete myserver`,
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Delete a context",
Run: func(cmd *cobra.Command, args []string) {
Name := args[0]
instances := viper.Get("instances").([]interface{})
for i, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
instances = slices.Delete(instances, i, i+1)
viper.Set("instances", instances)
viper.WriteConfig()
if instanceMap["default"] == true {
if len(instances) > 0 {
instances[0].(map[string]interface{})["default"] = true
viper.Set("instances", instances)
viper.WriteConfig()
newDefaultName := instances[0].(map[string]interface{})["name"]
fmt.Printf("Context '%s' deleted. '%s' is now the default context.\n", Name, newDefaultName)
} else {
fmt.Printf("Context '%s' deleted. No contexts remaining.\n", Name)
}
} else {
fmt.Printf("Context '%s' deleted.\n", Name)
}
return
}
}
fmt.Printf("Context '%s' not found.\n", Name)
},
}
}
+69
View File
@@ -0,0 +1,69 @@
package context
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/config"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// NewGetCommand creates the get command
func NewGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <context_name>",
Example: `context get myserver`,
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Get details of a specific context",
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
instancesRaw := viper.Get("instances")
if instancesRaw == nil {
instancesRaw = []any{}
}
instancesInterface := instancesRaw.([]any)
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// Convert interface{} to config.Instance structs
var instances []config.Instance
for _, item := range instancesInterface {
itemMap := item.(map[string]any)
instance := config.Instance{
Name: getString(itemMap, "name"),
FQDN: getString(itemMap, "fqdn"),
Token: getString(itemMap, "token"),
Default: getBool(itemMap, "default"),
}
instances = append(instances, instance)
}
// If a name was provided, filter to that single instance
var results []config.Instance
for _, inst := range instances {
if inst.Name == name {
results = append(results, inst)
break
}
}
if len(results) == 0 {
return fmt.Errorf("Context '%s' not found", name)
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(results)
},
}
}
+69
View File
@@ -0,0 +1,69 @@
package context
import (
"github.com/coollabsio/coolify-cli/internal/config"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// NewListCommand creates the list command
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all configured contexts",
RunE: func(cmd *cobra.Command, args []string) error {
// Get instances from viper (returns []interface{})
instancesRaw := viper.Get("instances")
if instancesRaw == nil {
instancesRaw = []interface{}{}
}
instancesInterface := instancesRaw.([]interface{})
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// Convert interface{} to config.Instance structs
var instances []config.Instance
for _, item := range instancesInterface {
itemMap := item.(map[string]any)
instance := config.Instance{
Name: getString(itemMap, "name"),
FQDN: getString(itemMap, "fqdn"),
Token: getString(itemMap, "token"),
Default: getBool(itemMap, "default"),
}
instances = append(instances, instance)
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(instances)
},
}
}
// Helper functions to safely extract values from map
func getString(m map[string]interface{}, key string) string {
if val, ok := m[key]; ok {
if str, ok := val.(string); ok {
return str
}
}
return ""
}
func getBool(m map[string]interface{}, key string) bool {
if val, ok := m[key]; ok {
if b, ok := val.(bool); ok {
return b
}
}
return false
}
+54
View File
@@ -0,0 +1,54 @@
package context
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// NewSetTokenCommand creates the set-token command
func NewSetDefaultCommand() *cobra.Command {
return &cobra.Command{
Use: "set-default <context_name>",
Example: `context set-default myserver`,
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Set a context as the default",
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
instances := viper.Get("instances").([]interface{})
// Check if instance exists
var found bool
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == name {
found = true
instanceMap["default"] = true
}
}
if !found {
return fmt.Errorf("Context '%s' not found", name)
} else {
// Only unset other defaults if we found the target instance
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] != name {
instanceMap["default"] = false
}
}
}
viper.Set("instances", instances)
if err := viper.WriteConfig(); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
// Show the list after updating
return NewListCommand().RunE(cmd, args)
},
}
}
+45
View File
@@ -0,0 +1,45 @@
package context
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// NewSetTokenCommand creates the set-token command
func NewSetTokenCommand() *cobra.Command {
return &cobra.Command{
Use: "set-token <context_name> <token>",
Example: `context set-token myserver your-new-api-token`,
Args: cli.ExactArgs(2, "<context_name> <token>"),
Short: "Update the API token for a context",
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
token := args[1]
var found interface{}
for _, instance := range viper.Get("instances").([]interface{}) {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == name {
found = instanceMap
break
}
}
if found == nil {
fmt.Printf("Context '%s' not found.\n", name)
return
}
instances := viper.Get("instances").([]interface{})
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == name {
instanceMap["token"] = token
}
}
viper.Set("instances", instances)
viper.WriteConfig()
fmt.Printf("Token updated for context '%s'.\n", name)
},
}
}
+95
View File
@@ -0,0 +1,95 @@
package context
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// NewUpdateCommand creates the update command
func NewUpdateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <context_name>",
Example: `context update myserver --name newname --url https://new.coolify.com --token newtoken`,
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Update a context's properties (name, URL, token)",
Run: func(cmd *cobra.Command, args []string) {
oldName := args[0]
instances := viper.Get("instances").([]interface{})
// Get flags
newName, _ := cmd.Flags().GetString("name")
newURL, _ := cmd.Flags().GetString("url")
newToken, _ := cmd.Flags().GetString("token")
// Check if at least one flag is provided
if newName == "" && newURL == "" && newToken == "" {
fmt.Println("Error: At least one of --name, --url, or --token must be provided")
fmt.Println("\nUsage: coolify context update <context_name> [--name <new_name>] [--url <new_url>] [--token <new_token>]")
return
}
// Find the context
var found bool
var contextToUpdate map[string]interface{}
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == oldName {
found = true
contextToUpdate = instanceMap
break
}
}
if !found {
fmt.Printf("Context '%s' not found.\n", oldName)
return
}
// If renaming, check if new name already exists
if newName != "" && newName != oldName {
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == newName {
fmt.Printf("Error: Context with name '%s' already exists.\n", newName)
return
}
}
contextToUpdate["name"] = newName
}
// Update URL if provided
if newURL != "" {
contextToUpdate["fqdn"] = newURL
}
// Update token if provided
if newToken != "" {
contextToUpdate["token"] = newToken
}
// Save changes
viper.Set("instances", instances)
err := viper.WriteConfig()
if err != nil {
fmt.Printf("Error saving config: %v\n", err)
return
}
// Use the new name if renamed, otherwise use old name
finalName := oldName
if newName != "" {
finalName = newName
}
fmt.Printf("Context '%s' updated successfully.\n", finalName)
},
}
cmd.Flags().StringP("name", "n", "", "New name for the context")
cmd.Flags().StringP("url", "u", "", "New URL for the context")
cmd.Flags().StringP("token", "t", "", "New token for the context")
return cmd
}
+55
View File
@@ -0,0 +1,55 @@
package context
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// NewUseCommand creates the use command
func NewUseCommand() *cobra.Command {
return &cobra.Command{
Use: "use <context_name>",
Example: `context use myserver`,
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Switch to a different context (set as default)",
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
instances := viper.Get("instances").([]interface{})
// Check if instance exists
var found bool
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == name {
found = true
break
}
}
if !found {
return fmt.Errorf("Context '%s' not found", name)
}
// Update default
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == name {
instanceMap["default"] = true
} else {
delete(instanceMap, "default")
}
}
viper.Set("instances", instances)
if err := viper.WriteConfig(); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
fmt.Printf("Switched to context '%s'.\n", name)
return nil
},
}
}
+41
View File
@@ -0,0 +1,41 @@
package context
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
)
// NewVerifyCommand creates the verify command for contexts
func NewVerifyCommand() *cobra.Command {
return &cobra.Command{
Use: "verify",
Short: "Verify current context connection and authentication",
Long: `Verify that the current context is properly configured by testing the connection
to the Coolify instance and validating the API token.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client - this will use the current default context
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Try to get version - this verifies both connection and authentication
version, err := client.GetVersion(ctx)
if err != nil {
return fmt.Errorf("verification failed: %w", err)
}
// If we got here, connection and authentication are working
fmt.Printf("✓ Connection successful\n")
fmt.Printf("✓ Authentication valid\n")
fmt.Printf("✓ Coolify version: %s\n", version)
return nil
},
}
}
+105
View File
@@ -0,0 +1,105 @@
package context
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/coollabsio/coolify-cli/internal/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestVerifyCommand_APIIntegration tests the verify logic using the API client directly
// This tests the core functionality that the verify command relies on
func TestVerifyCommand_APIIntegration(t *testing.T) {
t.Run("successful verification", func(t *testing.T) {
// Create a test HTTP server that responds to /api/v1/version
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/version", r.URL.Path)
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte("4.0.0-beta.383"))
}))
defer server.Close()
// Create API client and verify connection
client := api.NewClient(server.URL, "test-token")
version, err := client.GetVersion(context.Background())
// Verify results
require.NoError(t, err)
assert.Equal(t, "4.0.0-beta.383", version)
})
t.Run("unauthorized - invalid token", func(t *testing.T) {
// Create a test HTTP server that returns 401
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{
"message": "Invalid token",
})
}))
defer server.Close()
// Create API client with invalid token
client := api.NewClient(server.URL, "invalid-token")
_, err := client.GetVersion(context.Background())
// Verify error
require.Error(t, err)
assert.True(t, api.IsUnauthorized(err))
})
t.Run("server error", func(t *testing.T) {
// Create a test HTTP server that returns 500
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"error": "Internal server error",
})
}))
defer server.Close()
// Create API client
client := api.NewClient(server.URL, "test-token", api.WithRetries(0))
_, err := client.GetVersion(context.Background())
// Verify error
require.Error(t, err)
var apiErr *api.Error
require.ErrorAs(t, err, &apiErr)
assert.Equal(t, 500, apiErr.StatusCode)
})
t.Run("not found", func(t *testing.T) {
// Create a test HTTP server that returns 404
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{
"message": "Endpoint not found",
})
}))
defer server.Close()
// Create API client
client := api.NewClient(server.URL, "test-token")
_, err := client.GetVersion(context.Background())
// Verify error
require.Error(t, err)
assert.True(t, api.IsNotFound(err))
})
}
// TestNewVerifyCommand tests that the command is properly configured
func TestNewVerifyCommand(t *testing.T) {
cmd := NewVerifyCommand()
assert.Equal(t, "verify", cmd.Use)
assert.NotEmpty(t, cmd.Short)
assert.NotEmpty(t, cmd.Long)
assert.NotNil(t, cmd.RunE)
}
+35
View File
@@ -0,0 +1,35 @@
package context
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
)
// NewVersionCommand creates the version command for contexts
func NewVersionCommand() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Get current context's Coolify version",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Get version using API client
version, err := client.GetVersion(ctx)
if err != nil {
return fmt.Errorf("failed to get version: %w", err)
}
fmt.Println(version)
return nil
},
}
}
+124
View File
@@ -0,0 +1,124 @@
package backup
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewCreateCommand creates a new database
func NewCreateCommand() *cobra.Command {
createBackupCmd := &cobra.Command{
Use: "create <database_uuid>",
Short: "Create a new scheduled backup configuration",
Long: `Create a new scheduled backup configuration for a database. Configure frequency, retention, S3 storage, and other backup options.
Example: coolify database backup create abc123 --frequency "0 0 * * *" --enabled`,
Args: cli.ExactArgs(1, "<database_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
dbUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
req := &models.DatabaseBackupCreateRequest{}
// Apply flags if provided
if cmd.Flags().Changed("frequency") {
frequency, _ := cmd.Flags().GetString("frequency")
req.Frequency = &frequency
}
if cmd.Flags().Changed("enabled") {
enabled, _ := cmd.Flags().GetBool("enabled")
req.Enabled = &enabled
}
if cmd.Flags().Changed("save-s3") {
saveS3, _ := cmd.Flags().GetBool("save-s3")
req.SaveS3 = &saveS3
}
if cmd.Flags().Changed("s3-storage-uuid") {
s3UUID, _ := cmd.Flags().GetString("s3-storage-uuid")
req.S3StorageUUID = &s3UUID
}
if cmd.Flags().Changed("databases") {
databases, _ := cmd.Flags().GetString("databases")
req.DatabasesToBackup = &databases
}
if cmd.Flags().Changed("dump-all") {
dumpAll, _ := cmd.Flags().GetBool("dump-all")
req.DumpAll = &dumpAll
}
if cmd.Flags().Changed("retention-amount-locally") {
amount, _ := cmd.Flags().GetInt("retention-amount-locally")
req.DatabaseBackupRetentionAmountLocally = &amount
}
if cmd.Flags().Changed("retention-days-locally") {
days, _ := cmd.Flags().GetInt("retention-days-locally")
req.DatabaseBackupRetentionDaysLocally = &days
}
if cmd.Flags().Changed("retention-storage-locally") {
storage, _ := cmd.Flags().GetString("retention-storage-locally")
req.DatabaseBackupRetentionMaxStorageLocally = &storage
}
if cmd.Flags().Changed("retention-amount-s3") {
amount, _ := cmd.Flags().GetInt("retention-amount-s3")
req.DatabaseBackupRetentionAmountS3 = &amount
}
if cmd.Flags().Changed("retention-days-s3") {
days, _ := cmd.Flags().GetInt("retention-days-s3")
req.DatabaseBackupRetentionDaysS3 = &days
}
if cmd.Flags().Changed("retention-storage-s3") {
storage, _ := cmd.Flags().GetString("retention-storage-s3")
req.DatabaseBackupRetentionMaxStorageS3 = &storage
}
if cmd.Flags().Changed("timeout") {
timeout, _ := cmd.Flags().GetInt("timeout")
req.Timeout = &timeout
}
if cmd.Flags().Changed("disable-local") {
disableLocal, _ := cmd.Flags().GetBool("disable-local")
req.DisableLocalBackup = &disableLocal
}
dbService := service.NewDatabaseService(client)
backup, err := dbService.CreateBackup(ctx, dbUUID, req)
if err != nil {
return fmt.Errorf("failed to create backup: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(backup)
},
}
createBackupCmd.Flags().String("frequency", "", "Backup frequency (cron expression, e.g., '0 0 * * *' for daily)")
createBackupCmd.Flags().Bool("enabled", false, "Enable backup schedule")
createBackupCmd.Flags().Bool("save-s3", false, "Save backups to S3")
createBackupCmd.Flags().String("s3-storage-uuid", "", "S3 storage UUID")
createBackupCmd.Flags().String("databases-to-backup", "", "Comma-separated list of databases to backup")
createBackupCmd.Flags().Bool("dump-all", false, "Dump all databases")
createBackupCmd.Flags().Int("retention-amount-locally", 0, "Number of backups to retain locally")
createBackupCmd.Flags().Int("retention-days-locally", 0, "Days to retain backups locally")
createBackupCmd.Flags().String("retention-max-storage-locally", "", "Max storage for local backups (e.g., '1GB', '500MB')")
createBackupCmd.Flags().Int("retention-amount-s3", 0, "Number of backups to retain in S3")
createBackupCmd.Flags().Int("retention-days-s3", 0, "Days to retain backups in S3")
createBackupCmd.Flags().String("retention-max-storage-s3", "", "Max storage for S3 backups (e.g., '1GB', '500MB')")
createBackupCmd.Flags().Int("timeout", 0, "Backup timeout in seconds")
createBackupCmd.Flags().Bool("disable-local-backup", false, "Disable local backup storage")
return createBackupCmd
}
+63
View File
@@ -0,0 +1,63 @@
package backup
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewDeleteExecutionCommand lists all databases
func NewDeleteExecutionCommand() *cobra.Command {
deleteBackupExecutionCmd := &cobra.Command{
Use: "delete-execution <database_uuid> <backup_uuid> <execution_uuid>",
Short: "Delete backup execution",
Long: `Delete a specific backup execution and optionally from S3. First UUID is the database, second is the backup configuration, third is the specific execution.`,
Args: cli.ExactArgs(3, "<database_uuid> <backup_uuid> <execution_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
dbUUID := args[0]
backupUUID := args[1]
executionUUID := args[2]
force, _ := cmd.Flags().GetBool("force")
deleteS3, _ := cmd.Flags().GetBool("delete-s3")
if !force {
fmt.Printf("Are you sure you want to delete backup execution %s? (y/N): ", executionUUID)
reader := bufio.NewReader(os.Stdin)
response, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("error reading input: %w", err)
}
response = strings.TrimSpace(strings.ToLower(response))
if response != "y" && response != "yes" {
fmt.Println("Delete cancelled")
return nil
}
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
err = dbService.DeleteBackupExecution(ctx, dbUUID, backupUUID, executionUUID, deleteS3)
if err != nil {
return fmt.Errorf("failed to delete backup execution: %w", err)
}
fmt.Println("Backup execution deleted successfully")
return nil
},
}
deleteBackupExecutionCmd.Flags().Bool("delete-s3", false, "Delete backup file from S3")
return deleteBackupExecutionCmd
}
+62
View File
@@ -0,0 +1,62 @@
package backup
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewDeleteCommand deletes a database
func NewDeleteCommand() *cobra.Command {
deleteBackupCmd := &cobra.Command{
Use: "delete <database_uuid> <backup_uuid>",
Short: "Delete backup configuration",
Long: `Delete a backup configuration and optionally all its executions from S3. First UUID is the database, second is the specific backup configuration.`,
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
dbUUID := args[0]
backupUUID := args[1]
force, _ := cmd.Flags().GetBool("force")
deleteS3, _ := cmd.Flags().GetBool("delete-s3")
if !force {
fmt.Printf("Are you sure you want to delete backup configuration %s? (y/N): ", backupUUID)
reader := bufio.NewReader(os.Stdin)
response, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("error reading input: %w", err)
}
response = strings.TrimSpace(strings.ToLower(response))
if response != "y" && response != "yes" {
fmt.Println("Delete cancelled")
return nil
}
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
err = dbService.DeleteBackup(ctx, dbUUID, backupUUID, deleteS3)
if err != nil {
return fmt.Errorf("failed to delete backup: %w", err)
}
fmt.Println("Backup configuration deleted successfully")
return nil
},
}
deleteBackupCmd.Flags().Bool("delete-s3", false, "Delete backup files from S3")
return deleteBackupCmd
}
+45
View File
@@ -0,0 +1,45 @@
package backup
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewExecutionCommand lists all databases
func NewExecutionCommand() *cobra.Command {
return &cobra.Command{
Use: "executions <database_uuid> <backup_uuid>",
Short: "List backup executions",
Long: `List all executions for a backup configuration. First UUID is the database, second is the specific backup configuration.`,
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
dbUUID := args[0]
backupUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
executions, err := dbService.ListBackupExecutions(ctx, dbUUID, backupUUID)
if err != nil {
return fmt.Errorf("failed to list backup executions: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(executions)
},
}
}
+44
View File
@@ -0,0 +1,44 @@
package backup
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewListCommand lists all databases
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list <database_uuid>",
Short: "List all backup configurations for a database",
Long: `List all backup configurations for a specific database.`,
Args: cli.ExactArgs(1, "<database_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
dbUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
backups, err := dbService.ListBackups(ctx, dbUUID)
if err != nil {
return fmt.Errorf("failed to list backups: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(backups)
},
}
}
+46
View File
@@ -0,0 +1,46 @@
package backup
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewTriggerCommand triggers a database backup
func NewTriggerCommand() *cobra.Command {
return &cobra.Command{
Use: "trigger <database_uuid> <backup_uuid>",
Short: "Trigger immediate backup",
Long: `Trigger an immediate backup for a specific backup configuration. First UUID is the database, second is the specific backup configuration to trigger.`,
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
dbUUID := args[0]
backupUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
// Trigger immediate backup by updating with backup_now flag
req := &models.DatabaseBackupUpdateRequest{
BackupNow: cli.BoolPtr(true),
}
err = dbService.UpdateBackup(ctx, dbUUID, backupUUID, req)
if err != nil {
return fmt.Errorf("failed to trigger backup: %w", err)
}
fmt.Println("Immediate backup triggered successfully")
return nil
},
}
}
+125
View File
@@ -0,0 +1,125 @@
package backup
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewUpdateCommand updates a database
func NewUpdateCommand() *cobra.Command {
updateBackupCmd := &cobra.Command{
Use: "update <database_uuid> <backup_uuid>",
Short: "Update backup configuration",
Long: `Update a backup configuration settings (frequency, retention, S3, etc.). First UUID is the database, second is the specific backup configuration.`,
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
dbUUID := args[0]
backupUUID := args[1]
req := &models.DatabaseBackupUpdateRequest{}
hasChanges := false
if cmd.Flags().Changed("enabled") {
enabled, _ := cmd.Flags().GetBool("enabled")
req.Enabled = &enabled
hasChanges = true
}
if cmd.Flags().Changed("frequency") {
freq, _ := cmd.Flags().GetString("frequency")
req.Frequency = &freq
hasChanges = true
}
if cmd.Flags().Changed("save-s3") {
saveS3, _ := cmd.Flags().GetBool("save-s3")
req.SaveS3 = &saveS3
hasChanges = true
}
if cmd.Flags().Changed("s3-storage-uuid") {
s3UUID, _ := cmd.Flags().GetString("s3-storage-uuid")
req.S3StorageUUID = &s3UUID
hasChanges = true
}
if cmd.Flags().Changed("databases-to-backup") {
dbs, _ := cmd.Flags().GetString("databases-to-backup")
req.DatabasesToBackup = &dbs
hasChanges = true
}
if cmd.Flags().Changed("dump-all") {
dumpAll, _ := cmd.Flags().GetBool("dump-all")
req.DumpAll = &dumpAll
hasChanges = true
}
// Retention settings
if cmd.Flags().Changed("retention-amount-locally") {
amount, _ := cmd.Flags().GetInt("retention-amount-locally")
req.DatabaseBackupRetentionAmountLocally = &amount
hasChanges = true
}
if cmd.Flags().Changed("retention-days-locally") {
days, _ := cmd.Flags().GetInt("retention-days-locally")
req.DatabaseBackupRetentionDaysLocally = &days
hasChanges = true
}
if cmd.Flags().Changed("retention-max-storage-locally") {
storage, _ := cmd.Flags().GetInt("retention-max-storage-locally")
req.DatabaseBackupRetentionMaxStorageLocally = &storage
hasChanges = true
}
if cmd.Flags().Changed("retention-amount-s3") {
amount, _ := cmd.Flags().GetInt("retention-amount-s3")
req.DatabaseBackupRetentionAmountS3 = &amount
hasChanges = true
}
if cmd.Flags().Changed("retention-days-s3") {
days, _ := cmd.Flags().GetInt("retention-days-s3")
req.DatabaseBackupRetentionDaysS3 = &days
hasChanges = true
}
if cmd.Flags().Changed("retention-max-storage-s3") {
storage, _ := cmd.Flags().GetInt("retention-max-storage-s3")
req.DatabaseBackupRetentionMaxStorageS3 = &storage
hasChanges = true
}
if !hasChanges {
return fmt.Errorf("no fields to update")
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
err = dbService.UpdateBackup(ctx, dbUUID, backupUUID, req)
if err != nil {
return fmt.Errorf("failed to update backup: %w", err)
}
fmt.Println("Backup configuration updated successfully")
return nil
},
}
updateBackupCmd.Flags().Bool("enabled", false, "Enable or disable backup")
updateBackupCmd.Flags().String("frequency", "", "Backup frequency (cron expression)")
updateBackupCmd.Flags().Bool("save-s3", false, "Save backups to S3")
updateBackupCmd.Flags().String("s3-storage-uuid", "", "S3 storage UUID")
updateBackupCmd.Flags().String("databases-to-backup", "", "Comma-separated list of databases to backup")
updateBackupCmd.Flags().Bool("dump-all", false, "Dump all databases")
updateBackupCmd.Flags().Int("retention-amount-locally", 0, "Number of backups to retain locally")
updateBackupCmd.Flags().Int("retention-days-locally", 0, "Days to retain backups locally")
updateBackupCmd.Flags().Int("retention-max-storage-locally", 0, "Max storage for local backups (MB)")
updateBackupCmd.Flags().Int("retention-amount-s3", 0, "Number of backups to retain in S3")
updateBackupCmd.Flags().Int("retention-days-s3", 0, "Days to retain backups in S3")
updateBackupCmd.Flags().Int("retention-max-storage-s3", 0, "Max storage for S3 backups (MB)")
return updateBackupCmd
}
+287
View File
@@ -0,0 +1,287 @@
package database
import (
"context"
"fmt"
"strings"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewCreateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create <type>",
Short: "Create a new database",
Long: `Create a new database of the specified type.
Supported types: postgresql, mysql, mariadb, mongodb, redis, keydb, clickhouse, dragonfly
Examples:
coolify databases create postgresql --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production
coolify databases create mysql --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production --name="My MySQL"`,
Args: cli.ExactArgs(1, "<type>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
dbType := args[0]
validTypes := []string{"postgresql", "mysql", "mariadb", "mongodb", "redis", "keydb", "clickhouse", "dragonfly"}
isValid := false
for _, t := range validTypes {
if t == dbType {
isValid = true
break
}
}
if !isValid {
return fmt.Errorf("invalid database type '%s'. Valid types: %s", dbType, strings.Join(validTypes, ", "))
}
serverUUID, _ := cmd.Flags().GetString("server-uuid")
projectUUID, _ := cmd.Flags().GetString("project-uuid")
environmentName, _ := cmd.Flags().GetString("environment-name")
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
if serverUUID == "" || projectUUID == "" {
return fmt.Errorf("--server-uuid and --project-uuid are required")
}
if environmentName == "" && environmentUUID == "" {
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
}
req := &models.DatabaseCreateRequest{
ServerUUID: serverUUID,
ProjectUUID: projectUUID,
}
if environmentName != "" {
req.EnvironmentName = &environmentName
}
if environmentUUID != "" {
req.EnvironmentUUID = &environmentUUID
}
// Common flags
if cmd.Flags().Changed("name") {
name, _ := cmd.Flags().GetString("name")
req.Name = &name
}
if cmd.Flags().Changed("description") {
desc, _ := cmd.Flags().GetString("description")
req.Description = &desc
}
if cmd.Flags().Changed("image") {
image, _ := cmd.Flags().GetString("image")
req.Image = &image
}
if cmd.Flags().Changed("destination-uuid") {
dest, _ := cmd.Flags().GetString("destination-uuid")
req.DestinationUUID = &dest
}
if cmd.Flags().Changed("instant-deploy") {
instant, _ := cmd.Flags().GetBool("instant-deploy")
req.InstantDeploy = &instant
}
if cmd.Flags().Changed("is-public") {
isPublic, _ := cmd.Flags().GetBool("is-public")
req.IsPublic = &isPublic
}
if cmd.Flags().Changed("public-port") {
port, _ := cmd.Flags().GetInt("public-port")
req.PublicPort = &port
}
// Resource limits
if cmd.Flags().Changed("limits-memory") {
mem, _ := cmd.Flags().GetString("limits-memory")
req.LimitsMemory = &mem
}
if cmd.Flags().Changed("limits-cpus") {
cpus, _ := cmd.Flags().GetString("limits-cpus")
req.LimitsCpus = &cpus
}
// PostgreSQL specific
if dbType == "postgresql" {
if cmd.Flags().Changed("postgres-user") {
user, _ := cmd.Flags().GetString("postgres-user")
req.PostgresUser = &user
}
if cmd.Flags().Changed("postgres-password") {
pass, _ := cmd.Flags().GetString("postgres-password")
req.PostgresPassword = &pass
}
if cmd.Flags().Changed("postgres-db") {
db, _ := cmd.Flags().GetString("postgres-db")
req.PostgresDb = &db
}
}
// MySQL specific
if dbType == "mysql" {
if cmd.Flags().Changed("mysql-root-password") {
pass, _ := cmd.Flags().GetString("mysql-root-password")
req.MysqlRootPassword = &pass
}
if cmd.Flags().Changed("mysql-user") {
user, _ := cmd.Flags().GetString("mysql-user")
req.MysqlUser = &user
}
if cmd.Flags().Changed("mysql-password") {
pass, _ := cmd.Flags().GetString("mysql-password")
req.MysqlPassword = &pass
}
if cmd.Flags().Changed("mysql-database") {
db, _ := cmd.Flags().GetString("mysql-database")
req.MysqlDatabase = &db
}
}
// MariaDB specific
if dbType == "mariadb" {
if cmd.Flags().Changed("mariadb-root-password") {
pass, _ := cmd.Flags().GetString("mariadb-root-password")
req.MariadbRootPassword = &pass
}
if cmd.Flags().Changed("mariadb-user") {
user, _ := cmd.Flags().GetString("mariadb-user")
req.MariadbUser = &user
}
if cmd.Flags().Changed("mariadb-password") {
pass, _ := cmd.Flags().GetString("mariadb-password")
req.MariadbPassword = &pass
}
if cmd.Flags().Changed("mariadb-database") {
db, _ := cmd.Flags().GetString("mariadb-database")
req.MariadbDatabase = &db
}
}
// MongoDB specific
if dbType == "mongodb" {
if cmd.Flags().Changed("mongo-root-username") {
user, _ := cmd.Flags().GetString("mongo-root-username")
req.MongoInitdbRootUsername = &user
}
if cmd.Flags().Changed("mongo-root-password") {
pass, _ := cmd.Flags().GetString("mongo-root-password")
req.MongoInitdbRootPassword = &pass
}
if cmd.Flags().Changed("mongo-database") {
db, _ := cmd.Flags().GetString("mongo-database")
req.MongoInitdbDatabase = &db
}
}
// Redis specific
if dbType == "redis" {
if cmd.Flags().Changed("redis-password") {
pass, _ := cmd.Flags().GetString("redis-password")
req.RedisPassword = &pass
}
}
// KeyDB specific
if dbType == "keydb" {
if cmd.Flags().Changed("keydb-password") {
pass, _ := cmd.Flags().GetString("keydb-password")
req.KeydbPassword = &pass
}
}
// Clickhouse specific
if dbType == "clickhouse" {
if cmd.Flags().Changed("clickhouse-admin-user") {
user, _ := cmd.Flags().GetString("clickhouse-admin-user")
req.ClickhouseAdminUser = &user
}
if cmd.Flags().Changed("clickhouse-admin-password") {
pass, _ := cmd.Flags().GetString("clickhouse-admin-password")
req.ClickhouseAdminPassword = &pass
}
}
// Dragonfly specific
if dbType == "dragonfly" {
if cmd.Flags().Changed("dragonfly-password") {
pass, _ := cmd.Flags().GetString("dragonfly-password")
req.DragonflyPassword = &pass
}
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
database, err := dbService.Create(ctx, dbType, req)
if err != nil {
return fmt.Errorf("failed to create database: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(database)
},
}
// Common flags
cmd.Flags().String("server-uuid", "", "Server UUID (required)")
cmd.Flags().String("project-uuid", "", "Project UUID (required)")
cmd.Flags().String("environment-name", "", "Environment name")
cmd.Flags().String("environment-uuid", "", "Environment UUID")
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
cmd.Flags().String("name", "", "Database name")
cmd.Flags().String("description", "", "Database description")
cmd.Flags().String("image", "", "Docker image")
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
cmd.Flags().Bool("is-public", false, "Make database publicly accessible")
cmd.Flags().Int("public-port", 0, "Public port")
cmd.Flags().String("limits-memory", "", "Memory limit (e.g., '512m', '2g')")
cmd.Flags().String("limits-cpus", "", "CPU limit (e.g., '0.5', '2')")
// PostgreSQL flags
cmd.Flags().String("postgres-user", "", "PostgreSQL user")
cmd.Flags().String("postgres-password", "", "PostgreSQL password")
cmd.Flags().String("postgres-db", "", "PostgreSQL database name")
// MySQL flags
cmd.Flags().String("mysql-root-password", "", "MySQL root password")
cmd.Flags().String("mysql-user", "", "MySQL user")
cmd.Flags().String("mysql-password", "", "MySQL password")
cmd.Flags().String("mysql-database", "", "MySQL database name")
// MariaDB flags
cmd.Flags().String("mariadb-root-password", "", "MariaDB root password")
cmd.Flags().String("mariadb-user", "", "MariaDB user")
cmd.Flags().String("mariadb-password", "", "MariaDB password")
cmd.Flags().String("mariadb-database", "", "MariaDB database name")
// MongoDB flags
cmd.Flags().String("mongo-root-username", "", "MongoDB root username")
cmd.Flags().String("mongo-root-password", "", "MongoDB root password")
cmd.Flags().String("mongo-database", "", "MongoDB database name")
// Redis flags
cmd.Flags().String("redis-password", "", "Redis password")
// KeyDB flags
cmd.Flags().String("keydb-password", "", "KeyDB password")
// Clickhouse flags
cmd.Flags().String("clickhouse-admin-user", "", "Clickhouse admin user")
cmd.Flags().String("clickhouse-admin-password", "", "Clickhouse admin password")
// Dragonfly flags
cmd.Flags().String("dragonfly-password", "", "Dragonfly password")
return cmd
}
+43
View File
@@ -0,0 +1,43 @@
package database
import (
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/cmd/database/backup"
)
// NewDatabaseCommand creates the database parent command with all subcommands
func NewDatabaseCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "database",
Aliases: []string{"databases", "db", "dbs"},
Short: "Manage Coolify databases",
Long: `Manage Coolify databases (PostgreSQL, MySQL, MongoDB, Redis, MariaDB, KeyDB, Clickhouse, Dragonfly).`,
}
// Add main database commands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewStartCommand())
cmd.AddCommand(NewStopCommand())
cmd.AddCommand(NewRestartCommand())
cmd.AddCommand(NewCreateCommand())
cmd.AddCommand(NewUpdateCommand())
cmd.AddCommand(NewDeleteCommand())
// Add backup subcommand
backupCmd := &cobra.Command{
Use: "backup",
Short: "Manage database backups",
}
backupCmd.AddCommand(backup.NewCreateCommand())
backupCmd.AddCommand(backup.NewListCommand())
backupCmd.AddCommand(backup.NewDeleteCommand())
backupCmd.AddCommand(backup.NewUpdateCommand())
backupCmd.AddCommand(backup.NewTriggerCommand())
backupCmd.AddCommand(backup.NewExecutionCommand())
backupCmd.AddCommand(backup.NewDeleteExecutionCommand())
cmd.AddCommand(backupCmd)
return cmd
}
+68
View File
@@ -0,0 +1,68 @@
package database
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewDeleteCommand deletes a database
func NewDeleteCommand() *cobra.Command {
deleteDatabaseCmd := &cobra.Command{
Use: "delete <uuid>",
Short: "Delete a database",
Long: `Delete a database and optionally clean up its configurations, volumes, and networks.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
force, _ := cmd.Flags().GetBool("force")
deleteConfigurations, _ := cmd.Flags().GetBool("delete-configurations")
deleteVolumes, _ := cmd.Flags().GetBool("delete-volumes")
dockerCleanup, _ := cmd.Flags().GetBool("docker-cleanup")
deleteConnectedNetworks, _ := cmd.Flags().GetBool("delete-connected-networks")
if !force {
fmt.Printf("Are you sure you want to delete database %s? (y/N): ", uuid)
reader := bufio.NewReader(os.Stdin)
response, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("error reading input: %w", err)
}
response = strings.TrimSpace(strings.ToLower(response))
if response != "y" && response != "yes" {
fmt.Println("Delete cancelled")
return nil
}
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
err = dbService.Delete(ctx, uuid, deleteConfigurations, deleteVolumes, dockerCleanup, deleteConnectedNetworks)
if err != nil {
return fmt.Errorf("failed to delete database: %w", err)
}
fmt.Println("Database deleted successfully")
return nil
},
}
deleteDatabaseCmd.Flags().Bool("delete-configurations", true, "Delete configurations")
deleteDatabaseCmd.Flags().Bool("delete-volumes", true, "Delete volumes")
deleteDatabaseCmd.Flags().Bool("docker-cleanup", true, "Run docker cleanup")
deleteDatabaseCmd.Flags().Bool("delete-connected-networks", true, "Delete connected networks")
return deleteDatabaseCmd
}
+46
View File
@@ -0,0 +1,46 @@
package database
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewGetCommand gets database details
func NewGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <uuid>",
Short: "Get database details",
Long: `Get detailed information about a specific database by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
database, err := dbService.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get database: %w", err)
}
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter("table", output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(database)
},
}
}
+41
View File
@@ -0,0 +1,41 @@
package database
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewListCommand lists all databases
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all databases",
Long: `List all databases in Coolify.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
databases, err := dbService.List(ctx)
if err != nil {
return fmt.Errorf("failed to list databases: %w", err)
}
formatter, err := output.NewFormatter("table", output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(databases)
},
}
}
+38
View File
@@ -0,0 +1,38 @@
package database
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewRestartCommand restarts a database
func NewRestartCommand() *cobra.Command {
return &cobra.Command{
Use: "restart <uuid>",
Short: "Restart a database",
Long: `Restart a database by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
response, err := dbService.Restart(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to restart database: %w", err)
}
fmt.Println(response.Message)
return nil
},
}
}
+38
View File
@@ -0,0 +1,38 @@
package database
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewStartCommand starts a database
func NewStartCommand() *cobra.Command {
return &cobra.Command{
Use: "start <uuid>",
Short: "Start a database",
Long: `Start a database by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
response, err := dbService.Start(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to start database: %w", err)
}
fmt.Println(response.Message)
return nil
},
}
}
+38
View File
@@ -0,0 +1,38 @@
package database
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewStopCommand stops a database
func NewStopCommand() *cobra.Command {
return &cobra.Command{
Use: "stop <uuid>",
Short: "Stop a database",
Long: `Stop a database by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
response, err := dbService.Stop(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to stop database: %w", err)
}
fmt.Println(response.Message)
return nil
},
}
}
+116
View File
@@ -0,0 +1,116 @@
package database
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewUpdateCommand updates a database
func NewUpdateCommand() *cobra.Command {
updateDatabaseCmd := &cobra.Command{
Use: "update <uuid>",
Short: "Update a database",
Long: `Update a database's configuration by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
req := &models.DatabaseUpdateRequest{}
hasChanges := false
if cmd.Flags().Changed("name") {
name, _ := cmd.Flags().GetString("name")
req.Name = &name
hasChanges = true
}
if cmd.Flags().Changed("description") {
desc, _ := cmd.Flags().GetString("description")
req.Description = &desc
hasChanges = true
}
if cmd.Flags().Changed("image") {
image, _ := cmd.Flags().GetString("image")
req.Image = &image
hasChanges = true
}
if cmd.Flags().Changed("is-public") {
isPublic, _ := cmd.Flags().GetBool("is-public")
req.IsPublic = &isPublic
hasChanges = true
}
if cmd.Flags().Changed("public-port") {
port, _ := cmd.Flags().GetInt("public-port")
req.PublicPort = &port
hasChanges = true
}
// Resource limits
if cmd.Flags().Changed("limits-memory") {
mem, _ := cmd.Flags().GetString("limits-memory")
req.LimitsMemory = &mem
hasChanges = true
}
if cmd.Flags().Changed("limits-cpus") {
cpus, _ := cmd.Flags().GetString("limits-cpus")
req.LimitsCpus = &cpus
hasChanges = true
}
if !hasChanges {
return fmt.Errorf("no fields to update")
}
// Validate is-public requires public-port
if req.IsPublic != nil && *req.IsPublic {
// If setting to public, check if port is provided or fetch current database to check existing port
if req.PublicPort == nil || *req.PublicPort == 0 {
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
currentDB, err := dbService.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get current database: %w", err)
}
// Check if database already has a public port
if currentDB.PublicPort == nil || *currentDB.PublicPort == 0 {
return fmt.Errorf("cannot set database as public without a public port. Please provide --public-port")
}
}
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
err = dbService.Update(ctx, uuid, req)
if err != nil {
return fmt.Errorf("failed to update database: %w", err)
}
fmt.Println("Database updated successfully")
return nil
},
}
updateDatabaseCmd.Flags().String("name", "", "Database name")
updateDatabaseCmd.Flags().String("description", "", "Database description")
updateDatabaseCmd.Flags().String("image", "", "Docker image")
updateDatabaseCmd.Flags().Bool("is-public", false, "Make database publicly accessible")
updateDatabaseCmd.Flags().Int("public-port", 0, "Public port")
updateDatabaseCmd.Flags().String("limits-memory", "", "Memory limit")
updateDatabaseCmd.Flags().String("limits-cpus", "", "CPU limit")
return updateDatabaseCmd
}
-1012
View File
File diff suppressed because it is too large Load Diff
-357
View File
@@ -1,357 +0,0 @@
package cmd
import (
"context"
"fmt"
"strings"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
var deployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy related commands",
}
// DeployResultDisplay represents a deploy result for table display
type DeployResultDisplay struct {
Message string `json:"message"`
DeploymentUUID string `json:"deployment_uuid"`
}
var deployByUuidCmd = &cobra.Command{
Use: "uuid <uuid>",
Short: "Deploy by uuid",
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
deploySvc := service.NewDeploymentService(client)
result, err := deploySvc.Deploy(ctx, uuid, force)
if err != nil {
return fmt.Errorf("failed to deploy resource: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
// For table format, convert deployment info array to display format
if format == output.FormatTable {
displays := make([]DeployResultDisplay, len(result.Deployments))
for i, dep := range result.Deployments {
displays[i] = DeployResultDisplay{
Message: dep.Message,
DeploymentUUID: dep.DeploymentUUID,
}
}
return formatter.Format(displays)
}
return formatter.Format(result)
},
}
var deployByNameCmd = &cobra.Command{
Use: "name <name>",
Short: "Deploy by resource name",
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
name := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Find resource by name
resourceSvc := service.NewResourceService(client)
resources, err := resourceSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list resources: %w", err)
}
var matchedUUID string
for _, r := range resources {
if r.Name == name {
matchedUUID = r.UUID
break
}
}
if matchedUUID == "" {
return fmt.Errorf("resource with name '%s' not found", name)
}
// Deploy using the found UUID
force, _ := cmd.Flags().GetBool("force")
deploySvc := service.NewDeploymentService(client)
result, err := deploySvc.Deploy(ctx, matchedUUID, force)
if err != nil {
return fmt.Errorf("failed to deploy resource: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
// For table format, convert deployment info array to display format
if format == output.FormatTable {
displays := make([]DeployResultDisplay, len(result.Deployments))
for i, dep := range result.Deployments {
displays[i] = DeployResultDisplay{
Message: dep.Message,
DeploymentUUID: dep.DeploymentUUID,
}
}
return formatter.Format(displays)
}
return formatter.Format(result)
},
}
var deployBatchCmd = &cobra.Command{
Use: "batch <name1,name2,...>",
Short: "Deploy multiple resources by name",
Long: `Deploy multiple resources at once.
Provide resource names as comma-separated values.
Example: coolify deploy batch app1,app2,app3`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
namesStr := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Parse comma-separated names
names := make([]string, 0)
for _, name := range strings.Split(namesStr, ",") {
name = strings.TrimSpace(name)
if name != "" {
names = append(names, name)
}
}
if len(names) == 0 {
return fmt.Errorf("no resource names provided")
}
// Find resources by name
resourceSvc := service.NewResourceService(client)
resources, err := resourceSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list resources: %w", err)
}
// Build map of name -> UUID
nameToUUID := make(map[string]string)
for _, r := range resources {
nameToUUID[r.Name] = r.UUID
}
// Validate all names exist
var notFound []string
for _, name := range names {
if _, exists := nameToUUID[name]; !exists {
notFound = append(notFound, name)
}
}
if len(notFound) > 0 {
return fmt.Errorf("resources not found: %v", notFound)
}
// Deploy all resources
force, _ := cmd.Flags().GetBool("force")
deploySvc := service.NewDeploymentService(client)
type result struct {
Name string
UUID string
Success bool
Message string
Error string
}
results := make([]result, 0, len(names))
for _, name := range names {
uuid := nameToUUID[name]
fmt.Printf("Deploying %s...\n", name)
res, err := deploySvc.Deploy(ctx, uuid, force)
if err != nil {
results = append(results, result{
Name: name,
UUID: uuid,
Success: false,
Error: err.Error(),
})
fmt.Printf(" ❌ Failed: %v\n", err)
} else {
// Get first deployment message from the array
message := ""
if len(res.Deployments) > 0 {
message = res.Deployments[0].Message
}
results = append(results, result{
Name: name,
UUID: uuid,
Success: true,
Message: message,
})
fmt.Printf(" ✅ Success: %s\n", message)
}
}
// Summary
successCount := 0
for _, r := range results {
if r.Success {
successCount++
}
}
fmt.Printf("\nBatch deployment complete: %d/%d succeeded\n", successCount, len(results))
if successCount < len(results) {
return fmt.Errorf("some deployments failed")
}
return nil
},
}
var listDeploymentsCmd = &cobra.Command{
Use: "list",
Short: "List all deployments",
Long: `List all currently running deployments across all resources.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
deploySvc := service.NewDeploymentService(client)
deployments, err := deploySvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list deployments: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(deployments)
},
}
var getDeploymentCmd = &cobra.Command{
Use: "get <uuid>",
Short: "Get deployment details by UUID",
Long: `Get detailed information about a specific deployment by its UUID.`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
deploySvc := service.NewDeploymentService(client)
deployment, err := deploySvc.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get deployment: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(deployment)
},
}
var cancelDeploymentCmd = &cobra.Command{
Use: "cancel <uuid>",
Short: "Cancel a deployment by UUID",
Long: `Cancel an in-progress deployment. This will stop the deployment process and clean up any temporary resources.`,
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to cancel deployment %s? (yes/no): ", uuid)
fmt.Scanln(&response)
if response != "yes" && response != "y" {
fmt.Println("Cancel aborted.")
return nil
}
}
deploySvc := service.NewDeploymentService(client)
result, err := deploySvc.Cancel(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to cancel deployment: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(result)
},
}
func init() {
deployByUuidCmd.Flags().Bool("force", false, "Force deployment")
deployByNameCmd.Flags().Bool("force", false, "Force deployment")
deployBatchCmd.Flags().Bool("force", false, "Force deployment")
cancelDeploymentCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
rootCmd.AddCommand(deployCmd)
deployCmd.AddCommand(deployByUuidCmd)
deployCmd.AddCommand(deployByNameCmd)
deployCmd.AddCommand(deployBatchCmd)
deployCmd.AddCommand(listDeploymentsCmd)
deployCmd.AddCommand(getDeploymentCmd)
deployCmd.AddCommand(cancelDeploymentCmd)
}
+131
View File
@@ -0,0 +1,131 @@
package deployment
import (
"context"
"fmt"
"strings"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewBatchCommand deploys multiple resources by name
func NewBatchCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "batch <name1,name2,...>",
Short: "Deploy multiple resources by name",
Long: `Deploy multiple resources at once.
Provide resource names as comma-separated values.
Example: coolify deploy batch app1,app2,app3`,
Args: cli.ExactArgs(1, "<names>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
namesStr := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Parse comma-separated names
names := make([]string, 0)
for _, name := range strings.Split(namesStr, ",") {
name = strings.TrimSpace(name)
if name != "" {
names = append(names, name)
}
}
if len(names) == 0 {
return fmt.Errorf("no resource names provided")
}
// Find resources by name
resourceSvc := service.NewResourceService(client)
resources, err := resourceSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list resources: %w", err)
}
// Build map of name -> UUID
nameToUUID := make(map[string]string)
for _, r := range resources {
nameToUUID[r.Name] = r.UUID
}
// Validate all names exist
var notFound []string
for _, name := range names {
if _, exists := nameToUUID[name]; !exists {
notFound = append(notFound, name)
}
}
if len(notFound) > 0 {
return fmt.Errorf("resources not found: %v", notFound)
}
// Deploy all resources
force, _ := cmd.Flags().GetBool("force")
deploySvc := service.NewDeploymentService(client)
type result struct {
Name string
UUID string
Success bool
Message string
Error string
}
results := make([]result, 0, len(names))
for _, name := range names {
uuid := nameToUUID[name]
fmt.Printf("Deploying %s...\n", name)
res, err := deploySvc.Deploy(ctx, uuid, force)
if err != nil {
results = append(results, result{
Name: name,
UUID: uuid,
Success: false,
Error: err.Error(),
})
fmt.Printf(" ❌ Failed: %v\n", err)
} else {
// Get first deployment message from the array
message := ""
if len(res.Deployments) > 0 {
message = res.Deployments[0].Message
}
results = append(results, result{
Name: name,
UUID: uuid,
Success: true,
Message: message,
})
fmt.Printf(" ✅ Success: %s\n", message)
}
}
// Summary
successCount := 0
for _, r := range results {
if r.Success {
successCount++
}
}
fmt.Printf("\nBatch deployment complete: %d/%d succeeded\n", successCount, len(results))
if successCount < len(results) {
return fmt.Errorf("some deployments failed")
}
return nil
},
}
cmd.Flags().Bool("force", false, "Force deployment")
return cmd
}
+61
View File
@@ -0,0 +1,61 @@
package deployment
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewCancelCommand cancels a deployment
func NewCancelCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "cancel <uuid>",
Short: "Cancel a deployment by UUID",
Long: `Cancel an in-progress deployment. This will stop the deployment process and clean up any temporary resources.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to cancel deployment %s? (yes/no): ", uuid)
fmt.Scanln(&response)
if response != "yes" && response != "y" {
fmt.Println("Cancel aborted.")
return nil
}
}
deploySvc := service.NewDeploymentService(client)
result, err := deploySvc.Cancel(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to cancel deployment: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(result)
},
}
cmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
return cmd
}
+21
View File
@@ -0,0 +1,21 @@
package deployment
import "github.com/spf13/cobra"
// NewDeploymentCommand creates the deployment parent command with all subcommands
func NewDeploymentCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "deploy",
Short: "Deploy related commands",
}
// Add all deployment subcommands
cmd.AddCommand(NewUUIDCommand())
cmd.AddCommand(NewNameCommand())
cmd.AddCommand(NewBatchCommand())
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewCancelCommand())
return cmd
}
+44
View File
@@ -0,0 +1,44 @@
package deployment
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewGetCommand gets deployment details
func NewGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <uuid>",
Short: "Get deployment details by UUID",
Long: `Get detailed information about a specific deployment by its UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
deploySvc := service.NewDeploymentService(client)
deployment, err := deploySvc.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get deployment: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(deployment)
},
}
}
+42
View File
@@ -0,0 +1,42 @@
package deployment
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewListCommand lists all deployments
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all deployments",
Long: `List all currently running deployments across all resources.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
deploySvc := service.NewDeploymentService(client)
deployments, err := deploySvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list deployments: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(deployments)
},
}
}
+79
View File
@@ -0,0 +1,79 @@
package deployment
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewNameCommand deploys a resource by name
func NewNameCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "name <resource_name>",
Short: "Deploy by resource name",
Args: cli.ExactArgs(1, "<resource_name>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
name := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Find resource by name
resourceSvc := service.NewResourceService(client)
resources, err := resourceSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list resources: %w", err)
}
var matchedUUID string
for _, r := range resources {
if r.Name == name {
matchedUUID = r.UUID
break
}
}
if matchedUUID == "" {
return fmt.Errorf("resource with name '%s' not found", name)
}
// Deploy using the found UUID
force, _ := cmd.Flags().GetBool("force")
deploySvc := service.NewDeploymentService(client)
result, err := deploySvc.Deploy(ctx, matchedUUID, force)
if err != nil {
return fmt.Errorf("failed to deploy resource: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
// For table format, convert deployment info array to display format
if format == output.FormatTable {
displays := make([]ResultDisplay, len(result.Deployments))
for i, dep := range result.Deployments {
displays[i] = ResultDisplay{
Message: dep.Message,
DeploymentUUID: dep.DeploymentUUID,
}
}
return formatter.Format(displays)
}
return formatter.Format(result)
},
}
cmd.Flags().Bool("force", false, "Force deployment")
return cmd
}
+65
View File
@@ -0,0 +1,65 @@
package deployment
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// ResultDisplay represents a deploy result for table display
type ResultDisplay struct {
Message string `json:"message"`
DeploymentUUID string `json:"deployment_uuid"`
}
// NewUUIDCommand deploys a resource by UUID
func NewUUIDCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "uuid <uuid>",
Short: "Deploy by uuid",
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
deploySvc := service.NewDeploymentService(client)
result, err := deploySvc.Deploy(ctx, uuid, force)
if err != nil {
return fmt.Errorf("failed to deploy resource: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
// For table format, convert deployment info array to display format
if format == output.FormatTable {
displays := make([]ResultDisplay, len(result.Deployments))
for i, dep := range result.Deployments {
displays[i] = ResultDisplay{
Message: dep.Message,
DeploymentUUID: dep.DeploymentUUID,
}
}
return formatter.Format(displays)
}
return formatter.Format(result)
},
}
cmd.Flags().Bool("force", false, "Force deployment")
return cmd
}
+7 -4
View File
@@ -35,7 +35,7 @@ The man pages will be written to the specified directory (default: ./man).`,
header := &doc.GenManHeader{
Title: "COOLIFY",
Section: "1",
Source: "Coolify CLI " + Version,
Source: "Coolify CLI",
}
if err := doc.GenManTree(rootCmd, header, outputDir); err != nil {
@@ -56,7 +56,9 @@ The man pages will be written to the specified directory (default: ./man).`,
}
var markdownCmd = &cobra.Command{
Use: "markdown",
Use: "markdown",
Aliases: []string{"md"},
Short: "Generate markdown documentation",
Long: `Generate markdown documentation for all Coolify CLI commands.
@@ -83,11 +85,12 @@ The markdown files will be written to the specified directory (default: ./docs).
},
}
func init() {
rootCmd.AddCommand(docsCmd)
func NewDocsCommand() *cobra.Command {
docsCmd.AddCommand(manCmd)
docsCmd.AddCommand(markdownCmd)
manCmd.Flags().StringP("output-dir", "o", "./man", "Output directory for man pages")
markdownCmd.Flags().StringP("output-dir", "o", "./docs", "Output directory for markdown files")
return docsCmd
}
-49
View File
@@ -1,49 +0,0 @@
package cmd
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
var domainsCmd = &cobra.Command{
Use: "domain",
Aliases: []string{"domains"},
Short: "Domain related commands",
Long: `List all domains configured across your Coolify resources.`,
}
var listDomainsCmd = &cobra.Command{
Use: "list",
Short: "List all domains",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
domainSvc := service.NewDomainService(client)
domains, err := domainSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list domains: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(domains)
},
}
func init() {
rootCmd.AddCommand(domainsCmd)
domainsCmd.AddCommand(listDomainsCmd)
}
-393
View File
@@ -1,393 +0,0 @@
package cmd
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
var githubCmd = &cobra.Command{
Use: "github",
Aliases: []string{"gh", "github-app", "github-apps"},
Short: "Manage GitHub App integrations",
Long: `Manage GitHub App integrations for private repository deployments.`,
}
var listGitHubAppsCmd = &cobra.Command{
Use: "list",
Short: "List all GitHub App integrations",
Long: `List all GitHub App integrations configured in your Coolify instance.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
svc := service.NewGitHubAppService(client)
apps, err := svc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list GitHub Apps: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(apps)
},
}
var getGitHubAppCmd = &cobra.Command{
Use: "get <app_uuid>",
Short: "Get GitHub App details by UUID",
Long: `Get detailed information about a specific GitHub App integration.`,
Args: exactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
svc := service.NewGitHubAppService(client)
app, err := svc.Get(ctx, appUUID)
if err != nil {
return fmt.Errorf("failed to get GitHub App: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(app)
},
}
var createGitHubAppCmd = &cobra.Command{
Use: "create",
Short: "Create a GitHub App integration",
Long: `Create a new GitHub App integration. This allows you to deploy private repositories from GitHub.
Required flags: --name, --api-url, --html-url, --app-id, --installation-id, --client-id, --client-secret, --private-key-uuid
Example: coolify github create --name "My GitHub App" --api-url "https://api.github.com" --html-url "https://github.com" --app-id 123456 --installation-id 789012 --client-id "Iv1.abc123" --client-secret "secret123" --private-key-uuid "abc-123-def-456"`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
name, _ := cmd.Flags().GetString("name")
apiURL, _ := cmd.Flags().GetString("api-url")
htmlURL, _ := cmd.Flags().GetString("html-url")
appID, _ := cmd.Flags().GetInt("app-id")
installationID, _ := cmd.Flags().GetInt("installation-id")
clientID, _ := cmd.Flags().GetString("client-id")
clientSecret, _ := cmd.Flags().GetString("client-secret")
privateKeyUUID, _ := cmd.Flags().GetString("private-key-uuid")
req := &models.GitHubAppCreateRequest{
Name: name,
APIURL: apiURL,
HTMLURL: htmlURL,
AppID: appID,
InstallationID: installationID,
ClientID: clientID,
ClientSecret: clientSecret,
PrivateKeyUUID: privateKeyUUID,
}
// Optional fields
if cmd.Flags().Changed("organization") {
org, _ := cmd.Flags().GetString("organization")
req.Organization = &org
}
if cmd.Flags().Changed("custom-user") {
user, _ := cmd.Flags().GetString("custom-user")
req.CustomUser = &user
}
if cmd.Flags().Changed("custom-port") {
port, _ := cmd.Flags().GetInt("custom-port")
req.CustomPort = &port
}
if cmd.Flags().Changed("webhook-secret") {
secret, _ := cmd.Flags().GetString("webhook-secret")
req.WebhookSecret = &secret
}
if cmd.Flags().Changed("system-wide") {
systemWide, _ := cmd.Flags().GetBool("system-wide")
req.IsSystemWide = &systemWide
}
svc := service.NewGitHubAppService(client)
app, err := svc.Create(ctx, req)
if err != nil {
return fmt.Errorf("failed to create GitHub App: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(app)
},
}
var updateGitHubAppCmd = &cobra.Command{
Use: "update <app_uuid>",
Short: "Update a GitHub App integration",
Long: `Update an existing GitHub App integration. Provide the app UUID and the fields you want to update.`,
Args: exactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
req := &models.GitHubAppUpdateRequest{}
// Update only fields that were explicitly provided
if cmd.Flags().Changed("name") {
name, _ := cmd.Flags().GetString("name")
req.Name = &name
}
if cmd.Flags().Changed("organization") {
org, _ := cmd.Flags().GetString("organization")
req.Organization = &org
}
if cmd.Flags().Changed("api-url") {
apiURL, _ := cmd.Flags().GetString("api-url")
req.APIURL = &apiURL
}
if cmd.Flags().Changed("html-url") {
htmlURL, _ := cmd.Flags().GetString("html-url")
req.HTMLURL = &htmlURL
}
if cmd.Flags().Changed("custom-user") {
user, _ := cmd.Flags().GetString("custom-user")
req.CustomUser = &user
}
if cmd.Flags().Changed("custom-port") {
port, _ := cmd.Flags().GetInt("custom-port")
req.CustomPort = &port
}
if cmd.Flags().Changed("app-id") {
id, _ := cmd.Flags().GetInt("app-id")
req.AppID = &id
}
if cmd.Flags().Changed("installation-id") {
id, _ := cmd.Flags().GetInt("installation-id")
req.InstallationID = &id
}
if cmd.Flags().Changed("client-id") {
clientID, _ := cmd.Flags().GetString("client-id")
req.ClientID = &clientID
}
if cmd.Flags().Changed("client-secret") {
clientSecret, _ := cmd.Flags().GetString("client-secret")
req.ClientSecret = &clientSecret
}
if cmd.Flags().Changed("webhook-secret") {
secret, _ := cmd.Flags().GetString("webhook-secret")
req.WebhookSecret = &secret
}
if cmd.Flags().Changed("private-key-uuid") {
uuid, _ := cmd.Flags().GetString("private-key-uuid")
req.PrivateKeyUUID = &uuid
}
if cmd.Flags().Changed("system-wide") {
systemWide, _ := cmd.Flags().GetBool("system-wide")
req.IsSystemWide = &systemWide
}
svc := service.NewGitHubAppService(client)
err = svc.Update(ctx, appUUID, req)
if err != nil {
return fmt.Errorf("failed to update GitHub App: %w", err)
}
fmt.Println("GitHub App updated successfully")
return nil
},
}
var deleteGitHubAppCmd = &cobra.Command{
Use: "delete <app_uuid>",
Short: "Delete a GitHub App integration",
Long: `Delete a GitHub App integration. The app must not be used by any applications.`,
Args: exactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to delete GitHub App %s? This cannot be undone. (yes/no): ", appUUID)
fmt.Scanln(&response)
if response != "yes" && response != "y" {
fmt.Println("Delete cancelled.")
return nil
}
}
svc := service.NewGitHubAppService(client)
err = svc.Delete(ctx, appUUID)
if err != nil {
return fmt.Errorf("failed to delete GitHub App: %w", err)
}
fmt.Println("GitHub App deleted successfully")
return nil
},
}
var listRepositoriesCmd = &cobra.Command{
Use: "repos <app_uuid>",
Short: "List repositories accessible by a GitHub App",
Long: `List all repositories that are accessible by the specified GitHub App.`,
Args: exactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
svc := service.NewGitHubAppService(client)
repos, err := svc.ListRepositories(ctx, appUUID)
if err != nil {
return fmt.Errorf("failed to list repositories: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(repos)
},
}
var listBranchesCmd = &cobra.Command{
Use: "branches <app_uuid> <owner/repo>",
Short: "List branches for a repository",
Long: `List all branches for a specific repository. Provide the app UUID and repository in owner/repo format.
Example: coolify github branches abc-123-def owner/repository`,
Args: exactArgs(2, "<app_uuid> <owner/repo>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
// Parse owner/repo
ownerRepo := args[1]
parts := splitOwnerRepo(ownerRepo)
if len(parts) != 2 {
return fmt.Errorf("invalid repository format. Expected 'owner/repo', got '%s'", ownerRepo)
}
owner, repo := parts[0], parts[1]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
svc := service.NewGitHubAppService(client)
branches, err := svc.ListBranches(ctx, appUUID, owner, repo)
if err != nil {
return fmt.Errorf("failed to list branches: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(branches)
},
}
func init() {
// Create command flags
createGitHubAppCmd.Flags().String("name", "", "GitHub App name (required)")
createGitHubAppCmd.Flags().String("organization", "", "GitHub organization")
createGitHubAppCmd.Flags().String("api-url", "", "GitHub API URL (required, e.g., https://api.github.com)")
createGitHubAppCmd.Flags().String("html-url", "", "GitHub HTML URL (required, e.g., https://github.com)")
createGitHubAppCmd.Flags().String("custom-user", "", "Custom user for SSH (default: git)")
createGitHubAppCmd.Flags().Int("custom-port", 0, "Custom port for SSH (default: 22)")
createGitHubAppCmd.Flags().Int("app-id", 0, "GitHub App ID (required)")
createGitHubAppCmd.Flags().Int("installation-id", 0, "GitHub Installation ID (required)")
createGitHubAppCmd.Flags().String("client-id", "", "GitHub OAuth Client ID (required)")
createGitHubAppCmd.Flags().String("client-secret", "", "GitHub OAuth Client Secret (required)")
createGitHubAppCmd.Flags().String("webhook-secret", "", "GitHub Webhook Secret")
createGitHubAppCmd.Flags().String("private-key-uuid", "", "UUID of existing private key (required)")
createGitHubAppCmd.Flags().Bool("system-wide", false, "Is this app system-wide (cloud only)")
createGitHubAppCmd.MarkFlagRequired("name")
createGitHubAppCmd.MarkFlagRequired("api-url")
createGitHubAppCmd.MarkFlagRequired("html-url")
createGitHubAppCmd.MarkFlagRequired("app-id")
createGitHubAppCmd.MarkFlagRequired("installation-id")
createGitHubAppCmd.MarkFlagRequired("client-id")
createGitHubAppCmd.MarkFlagRequired("client-secret")
createGitHubAppCmd.MarkFlagRequired("private-key-uuid")
// Update command flags (all optional)
updateGitHubAppCmd.Flags().String("name", "", "GitHub App name")
updateGitHubAppCmd.Flags().String("organization", "", "GitHub organization")
updateGitHubAppCmd.Flags().String("api-url", "", "GitHub API URL")
updateGitHubAppCmd.Flags().String("html-url", "", "GitHub HTML URL")
updateGitHubAppCmd.Flags().String("custom-user", "", "Custom user for SSH")
updateGitHubAppCmd.Flags().Int("custom-port", 0, "Custom port for SSH")
updateGitHubAppCmd.Flags().Int("app-id", 0, "GitHub App ID")
updateGitHubAppCmd.Flags().Int("installation-id", 0, "GitHub Installation ID")
updateGitHubAppCmd.Flags().String("client-id", "", "GitHub OAuth Client ID")
updateGitHubAppCmd.Flags().String("client-secret", "", "GitHub OAuth Client Secret")
updateGitHubAppCmd.Flags().String("webhook-secret", "", "GitHub Webhook Secret")
updateGitHubAppCmd.Flags().String("private-key-uuid", "", "UUID of private key")
updateGitHubAppCmd.Flags().Bool("system-wide", false, "Is this app system-wide")
// Delete command flags
deleteGitHubAppCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
rootCmd.AddCommand(githubCmd)
githubCmd.AddCommand(listGitHubAppsCmd)
githubCmd.AddCommand(getGitHubAppCmd)
githubCmd.AddCommand(createGitHubAppCmd)
githubCmd.AddCommand(updateGitHubAppCmd)
githubCmd.AddCommand(deleteGitHubAppCmd)
githubCmd.AddCommand(listRepositoriesCmd)
githubCmd.AddCommand(listBranchesCmd)
}
+53
View File
@@ -0,0 +1,53 @@
package github
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewListBranchesCommand() *cobra.Command {
return &cobra.Command{
Use: "branches <app_uuid> <owner/repo>",
Short: "List branches for a repository",
Long: `List all branches for a specific repository. Provide the app UUID and repository in owner/repo format.
Example: coolify github branches abc-123-def owner/repository`,
Args: cli.ExactArgs(2, "<app_uuid> <owner/repo>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
// Parse owner/repo
ownerRepo := args[1]
parts := cli.SplitOwnerRepo(ownerRepo)
if len(parts) != 2 {
return fmt.Errorf("invalid repository format. Expected 'owner/repo', got '%s'", ownerRepo)
}
owner, repo := parts[0], parts[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
svc := service.NewGitHubAppService(client)
branches, err := svc.ListBranches(ctx, appUUID, owner, repo)
if err != nil {
return fmt.Errorf("failed to list branches: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(branches)
},
}
}
+113
View File
@@ -0,0 +1,113 @@
package github
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewCreateCommand() *cobra.Command {
createCmd := &cobra.Command{
Use: "create",
Short: "Create a GitHub App integration",
Long: `Create a new GitHub App integration. This allows you to deploy private repositories from GitHub.
Required flags: --name, --api-url, --html-url, --app-id, --installation-id, --client-id, --client-secret, --private-key-uuid
Example: coolify github create --name "My GitHub App" --api-url "https://api.github.com" --html-url "https://github.com" --app-id 123456 --installation-id 789012 --client-id "Iv1.abc123" --client-secret "secret123" --private-key-uuid "abc-123-def-456"`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
name, _ := cmd.Flags().GetString("name")
apiURL, _ := cmd.Flags().GetString("api-url")
htmlURL, _ := cmd.Flags().GetString("html-url")
appID, _ := cmd.Flags().GetInt("app-id")
installationID, _ := cmd.Flags().GetInt("installation-id")
clientID, _ := cmd.Flags().GetString("client-id")
clientSecret, _ := cmd.Flags().GetString("client-secret")
privateKeyUUID, _ := cmd.Flags().GetString("private-key-uuid")
req := &models.GitHubAppCreateRequest{
Name: name,
APIURL: apiURL,
HTMLURL: htmlURL,
AppID: appID,
InstallationID: installationID,
ClientID: clientID,
ClientSecret: clientSecret,
PrivateKeyUUID: privateKeyUUID,
}
// Optional fields
if cmd.Flags().Changed("organization") {
org, _ := cmd.Flags().GetString("organization")
req.Organization = &org
}
if cmd.Flags().Changed("custom-user") {
user, _ := cmd.Flags().GetString("custom-user")
req.CustomUser = &user
}
if cmd.Flags().Changed("custom-port") {
port, _ := cmd.Flags().GetInt("custom-port")
req.CustomPort = &port
}
if cmd.Flags().Changed("webhook-secret") {
secret, _ := cmd.Flags().GetString("webhook-secret")
req.WebhookSecret = &secret
}
if cmd.Flags().Changed("system-wide") {
systemWide, _ := cmd.Flags().GetBool("system-wide")
req.IsSystemWide = &systemWide
}
svc := service.NewGitHubAppService(client)
app, err := svc.Create(ctx, req)
if err != nil {
return fmt.Errorf("failed to create GitHub App: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(app)
},
}
createCmd.Flags().String("name", "", "GitHub App name (required)")
createCmd.Flags().String("organization", "", "GitHub organization")
createCmd.Flags().String("api-url", "", "GitHub API URL (required, e.g., https://api.github.com)")
createCmd.Flags().String("html-url", "", "GitHub HTML URL (required, e.g., https://github.com)")
createCmd.Flags().String("custom-user", "", "Custom user for SSH (default: git)")
createCmd.Flags().Int("custom-port", 0, "Custom port for SSH (default: 22)")
createCmd.Flags().Int("app-id", 0, "GitHub App ID (required)")
createCmd.Flags().Int("installation-id", 0, "GitHub Installation ID (required)")
createCmd.Flags().String("client-id", "", "GitHub OAuth Client ID (required)")
createCmd.Flags().String("client-secret", "", "GitHub OAuth Client Secret (required)")
createCmd.Flags().String("webhook-secret", "", "GitHub Webhook Secret")
createCmd.Flags().String("private-key-uuid", "", "UUID of existing private key (required)")
createCmd.Flags().Bool("system-wide", false, "Is this app system-wide (cloud only)")
createCmd.MarkFlagRequired("name")
createCmd.MarkFlagRequired("api-url")
createCmd.MarkFlagRequired("html-url")
createCmd.MarkFlagRequired("app-id")
createCmd.MarkFlagRequired("installation-id")
createCmd.MarkFlagRequired("client-id")
createCmd.MarkFlagRequired("client-secret")
createCmd.MarkFlagRequired("private-key-uuid")
return createCmd
}
+55
View File
@@ -0,0 +1,55 @@
package github
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewDeleteCommand() *cobra.Command {
deleteCmd := &cobra.Command{
Use: "delete <app_uuid>",
Short: "Delete a GitHub App integration",
Long: `Delete a GitHub App integration. The app must not be used by any applications.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to delete GitHub App %s? This cannot be undone. (yes/no): ", appUUID)
fmt.Scanln(&response)
if response != "yes" && response != "y" {
fmt.Println("Delete cancelled.")
return nil
}
}
svc := service.NewGitHubAppService(client)
err = svc.Delete(ctx, appUUID)
if err != nil {
return fmt.Errorf("failed to delete GitHub App: %w", err)
}
fmt.Println("GitHub App deleted successfully")
return nil
},
}
deleteCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
return deleteCmd
}
+43
View File
@@ -0,0 +1,43 @@
package github
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <app_uuid>",
Short: "Get GitHub App details by UUID",
Long: `Get detailed information about a specific GitHub App integration.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
svc := service.NewGitHubAppService(client)
app, err := svc.Get(ctx, appUUID)
if err != nil {
return fmt.Errorf("failed to get GitHub App: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(app)
},
}
}
+25
View File
@@ -0,0 +1,25 @@
package github
import (
"github.com/spf13/cobra"
)
func NewGitHubCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "github",
Aliases: []string{"gh", "github-app", "github-apps"},
Short: "Manage GitHub App integrations",
Long: `Manage GitHub App integrations for private repository deployments.`,
}
// Add main database commands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewCreateCommand())
cmd.AddCommand(NewUpdateCommand())
cmd.AddCommand(NewDeleteCommand())
cmd.AddCommand(NewListRepositoriesCommand())
cmd.AddCommand(NewListBranchesCommand())
return cmd
}
+41
View File
@@ -0,0 +1,41 @@
package github
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all GitHub App integrations",
Long: `List all GitHub App integrations configured in Coolify.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
svc := service.NewGitHubAppService(client)
apps, err := svc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list GitHub Apps: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(apps)
},
}
}
+43
View File
@@ -0,0 +1,43 @@
package github
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewListRepositoriesCommand() *cobra.Command {
return &cobra.Command{
Use: "repos <app_uuid>",
Short: "List repositories accessible by a GitHub App",
Long: `List all repositories that are accessible by the specified GitHub App.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
svc := service.NewGitHubAppService(client)
repos, err := svc.ListRepositories(ctx, appUUID)
if err != nil {
return fmt.Errorf("failed to list repositories: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(repos)
},
}
}
+110
View File
@@ -0,0 +1,110 @@
package github
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewUpdateCommand() *cobra.Command {
updateCmd := &cobra.Command{
Use: "update <app_uuid>",
Short: "Update a GitHub App integration",
Long: `Update an existing GitHub App integration. Provide the app UUID and the fields you want to update.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
req := &models.GitHubAppUpdateRequest{}
// Update only fields that were explicitly provided
if cmd.Flags().Changed("name") {
name, _ := cmd.Flags().GetString("name")
req.Name = &name
}
if cmd.Flags().Changed("organization") {
org, _ := cmd.Flags().GetString("organization")
req.Organization = &org
}
if cmd.Flags().Changed("api-url") {
apiURL, _ := cmd.Flags().GetString("api-url")
req.APIURL = &apiURL
}
if cmd.Flags().Changed("html-url") {
htmlURL, _ := cmd.Flags().GetString("html-url")
req.HTMLURL = &htmlURL
}
if cmd.Flags().Changed("custom-user") {
user, _ := cmd.Flags().GetString("custom-user")
req.CustomUser = &user
}
if cmd.Flags().Changed("custom-port") {
port, _ := cmd.Flags().GetInt("custom-port")
req.CustomPort = &port
}
if cmd.Flags().Changed("app-id") {
id, _ := cmd.Flags().GetInt("app-id")
req.AppID = &id
}
if cmd.Flags().Changed("installation-id") {
id, _ := cmd.Flags().GetInt("installation-id")
req.InstallationID = &id
}
if cmd.Flags().Changed("client-id") {
clientID, _ := cmd.Flags().GetString("client-id")
req.ClientID = &clientID
}
if cmd.Flags().Changed("client-secret") {
clientSecret, _ := cmd.Flags().GetString("client-secret")
req.ClientSecret = &clientSecret
}
if cmd.Flags().Changed("webhook-secret") {
secret, _ := cmd.Flags().GetString("webhook-secret")
req.WebhookSecret = &secret
}
if cmd.Flags().Changed("private-key-uuid") {
uuid, _ := cmd.Flags().GetString("private-key-uuid")
req.PrivateKeyUUID = &uuid
}
if cmd.Flags().Changed("system-wide") {
systemWide, _ := cmd.Flags().GetBool("system-wide")
req.IsSystemWide = &systemWide
}
svc := service.NewGitHubAppService(client)
err = svc.Update(ctx, appUUID, req)
if err != nil {
return fmt.Errorf("failed to update GitHub App: %w", err)
}
fmt.Println("GitHub App updated successfully")
return nil
},
}
updateCmd.Flags().String("name", "", "GitHub App name")
updateCmd.Flags().String("organization", "", "GitHub organization")
updateCmd.Flags().String("api-url", "", "GitHub API URL")
updateCmd.Flags().String("html-url", "", "GitHub HTML URL")
updateCmd.Flags().String("custom-user", "", "Custom user for SSH")
updateCmd.Flags().Int("custom-port", 0, "Custom port for SSH")
updateCmd.Flags().Int("app-id", 0, "GitHub App ID")
updateCmd.Flags().Int("installation-id", 0, "GitHub Installation ID")
updateCmd.Flags().String("client-id", "", "GitHub OAuth Client ID")
updateCmd.Flags().String("client-secret", "", "GitHub OAuth Client Secret")
updateCmd.Flags().String("webhook-secret", "", "GitHub Webhook Secret")
updateCmd.Flags().String("private-key-uuid", "", "UUID of private key")
updateCmd.Flags().Bool("system-wide", false, "Is this app system-wide")
return updateCmd
}
-311
View File
@@ -1,311 +0,0 @@
package cmd
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var contextCmd = &cobra.Command{
Use: "context",
Short: "Manage Coolify contexts (instance configurations)",
Long: `Manage Coolify contexts. A context contains the configuration (URL and token) for a Coolify instance.`,
}
var contextVersionCmd = &cobra.Command{
Use: "version",
Short: "Get current context's Coolify version",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Get version using API client
version, err := client.GetVersion(ctx)
if err != nil {
return fmt.Errorf("failed to get version: %w", err)
}
fmt.Println(version)
return nil
},
}
var listContextsCmd = &cobra.Command{
Use: "list",
Short: "List all configured contexts",
Run: func(cmd *cobra.Command, args []string) {
instances := viper.Get("instances").([]interface{})
if PrettyMode {
var prettyJSON bytes.Buffer
instancesBytes, err := json.Marshal(instances)
if err != nil {
fmt.Println(err)
return
}
err = json.Indent(&prettyJSON, instancesBytes, "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(prettyJSON.String())
return
}
if JsonMode {
instancesBytes, err := json.Marshal(instances)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(instancesBytes))
return
}
fmt.Fprintln(w, "#\tName\tFqdn\tToken\tDefault")
for index, entry := range instances {
entryMap, ok := entry.(map[string]interface{})
if !ok {
fmt.Println("Error")
return
}
if ShowSensitive {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n", index+1, entryMap["name"], entryMap["fqdn"], entryMap["token"], map[bool]string{true: "true", false: ""}[entryMap["default"] == true])
} else {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n", index+1, entryMap["name"], entryMap["fqdn"], SensitiveInformationOverlay, map[bool]string{true: "true", false: ""}[entryMap["default"] == true])
}
}
w.Flush()
fmt.Println("\nNote: Use -s to show sensitive information.")
},
}
var addContextCmd = &cobra.Command{
Use: "add <name> <url> <token>",
Example: `context add myserver https://coolify.example.com your-api-token`,
Args: exactArgs(3, "<name> <url> <token>"),
Short: "Add a new context",
Run: func(cmd *cobra.Command, args []string) {
Name := args[0]
Host := args[1]
Token := args[2]
force, _ := cmd.Flags().GetBool("force")
instances := viper.Get("instances").([]interface{})
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
if force {
instanceMap["token"] = Token
if SetDefaultInstance {
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
delete(instanceMap, "default")
}
instanceMap["default"] = true
fmt.Printf("%s already exists. Force overwriting. Setting it as default. \n", Name)
} else {
fmt.Printf("%s already exists. Force overwriting. \n", Name)
}
viper.Set("instances", instances)
viper.WriteConfig()
return
}
fmt.Printf("%s already exists. \n", Name)
fmt.Println("\nNote: Use --force to force overwrite.")
return
}
}
instances = append(instances, map[string]interface{}{
"name": Name,
"fqdn": Host,
"token": Token,
})
if SetDefaultInstance {
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
delete(instanceMap, "default")
}
instances[len(instances)-1].(map[string]interface{})["default"] = true
}
viper.Set("instances", instances)
viper.WriteConfig()
listContextsCmd.Run(cmd, args)
},
}
var deleteContextCmd = &cobra.Command{
Use: "delete <name>",
Example: `context delete myserver`,
Args: exactArgs(1, "<name>"),
Short: "Delete a context",
Run: func(cmd *cobra.Command, args []string) {
Name := args[0]
instances := viper.Get("instances").([]interface{})
for i, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
instances = append(instances[:i], instances[i+1:]...)
viper.Set("instances", instances)
viper.WriteConfig()
fmt.Printf("%s removed. \n", Name)
if instanceMap["default"] == true {
fmt.Println("Note: The default instance has been removed.")
if len(instances) > 0 {
instances[0].(map[string]interface{})["default"] = true
viper.Set("instances", instances)
viper.WriteConfig()
fmt.Printf("%s set as default. \n", instances[0].(map[string]interface{})["fqdn"])
}
}
return
}
}
fmt.Printf("%s not found. \n", Name)
},
}
var setTokenCmd = &cobra.Command{
Use: "set-token <name> <token>",
Example: `context set-token myserver your-new-api-token`,
Args: exactArgs(2, "<name> <token>"),
Short: "Update the API token for a context",
Run: func(cmd *cobra.Command, args []string) {
Name = args[0]
Token = args[1]
var found interface{}
for _, instance := range viper.Get("instances").([]interface{}) {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
found = instanceMap
break
}
}
if found == nil {
fmt.Printf("%s instance is not found. \n", Name)
return
}
instances := viper.Get("instances").([]interface{})
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
instanceMap["token"] = Token
}
}
viper.Set("instances", instances)
viper.WriteConfig()
listContextsCmd.Run(cmd, args)
},
}
var useContextCmd = &cobra.Command{
Use: "use <name>",
Example: `context use myserver`,
Args: exactArgs(1, "<name>"),
Short: "Switch to a different context (set as default)",
Run: func(cmd *cobra.Command, args []string) {
Name := args[0]
instances := viper.Get("instances").([]interface{})
var found interface{}
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
found = instanceMap
break
}
}
if found == nil {
fmt.Printf("%s not found. \n", Name)
return
}
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
instanceMap["default"] = true
} else {
delete(instanceMap, "default")
}
}
viper.Set("instances", instances)
viper.WriteConfig()
listContextsCmd.Run(cmd, args)
},
}
var getContextCmd = &cobra.Command{
Use: "get <name>",
Example: `context get myserver`,
Args: exactArgs(1, "<name>"),
Short: "Get details of a specific context",
Run: func(cmd *cobra.Command, args []string) {
Name := args[0]
instances := viper.Get("instances").([]interface{})
if PrettyMode {
var prettyJSON bytes.Buffer
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
instanceMap["token"] = SensitiveInformationOverlay
}
instancesBytes, err := json.Marshal(instances)
if err != nil {
fmt.Println(err)
return
}
err = json.Indent(&prettyJSON, instancesBytes, "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(prettyJSON.String())
return
}
if JsonMode {
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
instanceMap["token"] = SensitiveInformationOverlay
}
instancesBytes, err := json.Marshal(instances)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(instancesBytes))
return
}
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
fmt.Fprintln(w, "Name\tHost\tToken")
if ShowSensitive {
fmt.Fprintf(w, "%s\t%s\t%s\n", Name, instanceMap["fqdn"], instanceMap["token"])
} else {
fmt.Fprintf(w, "%s\t%s\t%s\n", Name, instanceMap["fqdn"], SensitiveInformationOverlay)
}
w.Flush()
fmt.Println("\nNote: Use -s to show sensitive information.")
return
}
}
fmt.Printf("%s not found. \n", Name)
},
}
func init() {
addContextCmd.Flags().BoolVarP(&SetDefaultInstance, "default", "d", false, "Set as default context")
addContextCmd.Flags().BoolP("force", "f", false, "Force overwrite if context already exists")
rootCmd.AddCommand(contextCmd)
contextCmd.AddCommand(contextVersionCmd)
contextCmd.AddCommand(listContextsCmd)
contextCmd.AddCommand(addContextCmd)
contextCmd.AddCommand(deleteContextCmd)
contextCmd.AddCommand(setTokenCmd)
contextCmd.AddCommand(useContextCmd)
contextCmd.AddCommand(getContextCmd)
}
-132
View File
@@ -1,132 +0,0 @@
package cmd
import (
"context"
"fmt"
"os"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
var privateKeysCmd = &cobra.Command{
Use: "private-key",
Aliases: []string{"private-keys", "key", "keys"},
Short: "Private key related commands",
Long: `Manage SSH private keys for server authentication - list, add, and remove keys.`,
}
var listPrivateKeysCmd = &cobra.Command{
Use: "list",
Short: "List all private keys",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
keySvc := service.NewPrivateKeyService(client)
keys, err := keySvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list private keys: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
if err := formatter.Format(keys); err != nil {
return err
}
if !showSensitive && format == output.FormatTable {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
var addPrivateKeyCmd = &cobra.Command{
Use: "add <name> <private_key_or_file>",
Example: `add mykey ~/.ssh/id_rsa`,
Args: exactArgs(2, "<uuid1> <uuid2>"),
Short: "Add a private key",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
name := args[0]
privateKeyInput := args[1]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
var privateKey string
// Check if input is a file path
if _, err := os.Stat(privateKeyInput); err == nil {
keyBytes, err := os.ReadFile(privateKeyInput)
if err != nil {
return fmt.Errorf("error reading private key file: %w", err)
}
privateKey = string(keyBytes)
} else {
privateKey = privateKeyInput
}
keySvc := service.NewPrivateKeyService(client)
req := models.PrivateKeyCreateRequest{
Name: name,
PrivateKey: privateKey,
}
key, err := keySvc.Create(ctx, req)
if err != nil {
return fmt.Errorf("failed to add private key: %w", err)
}
fmt.Printf("Private key '%s' added successfully (UUID: %s)\n", key.Name, key.UUID)
return nil
},
}
var removePrivateKeyCmd = &cobra.Command{
Use: "remove <uuid>",
Args: exactArgs(1, "<uuid>"),
Short: "Remove a private key",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
keySvc := service.NewPrivateKeyService(client)
err = keySvc.Delete(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to remove private key: %w", err)
}
fmt.Println("Private key removed successfully")
return nil
},
}
func init() {
rootCmd.AddCommand(privateKeysCmd)
privateKeysCmd.AddCommand(listPrivateKeysCmd)
privateKeysCmd.AddCommand(addPrivateKeyCmd)
privateKeysCmd.AddCommand(removePrivateKeyCmd)
}
+58
View File
@@ -0,0 +1,58 @@
package privatekeys
import (
"context"
"fmt"
"os"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewCreateCommand creates the create command
func NewCreateCommand() *cobra.Command {
return &cobra.Command{
Use: "add <key_name> <private_key_or_file>",
Example: `add mykey ~/.ssh/id_rsa`,
Args: cli.ExactArgs(2, "<key_name> <private_key_or_file>"),
Short: "Add a private key",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
name := args[0]
privateKeyInput := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
var privateKey string
// Check if input is a file path
if _, err := os.Stat(privateKeyInput); err == nil {
keyBytes, err := os.ReadFile(privateKeyInput)
if err != nil {
return fmt.Errorf("error reading private key file: %w", err)
}
privateKey = string(keyBytes)
} else {
privateKey = privateKeyInput
}
keySvc := service.NewPrivateKeyService(client)
req := models.PrivateKeyCreateRequest{
Name: name,
PrivateKey: privateKey,
}
key, err := keySvc.Create(ctx, req)
if err != nil {
return fmt.Errorf("failed to add private key: %w", err)
}
fmt.Printf("Private key '%s' added successfully (UUID: %s)\n", key.Name, key.UUID)
return nil
},
}
}
+37
View File
@@ -0,0 +1,37 @@
package privatekeys
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewDeleteCommand creates the delete command
func NewDeleteCommand() *cobra.Command {
return &cobra.Command{
Use: "remove <uuid>",
Args: cli.ExactArgs(1, "<uuid>"),
Short: "Remove a private key",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
keySvc := service.NewPrivateKeyService(client)
err = keySvc.Delete(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to remove private key: %w", err)
}
fmt.Println("Private key removed successfully")
return nil
},
}
}
+53
View File
@@ -0,0 +1,53 @@
package privatekeys
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewListCommand creates the list command
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all private keys",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
keySvc := service.NewPrivateKeyService(client)
keys, err := keySvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list private keys: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
if err := formatter.Format(keys); err != nil {
return err
}
if !showSensitive && format == output.FormatTable {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
}
+22
View File
@@ -0,0 +1,22 @@
package privatekeys
import (
"github.com/spf13/cobra"
)
// NewDomainsCommand creates the domains parent command
func NewPrivateKeysCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "private-key",
Aliases: []string{"private-keys", "key", "keys"},
Short: "Private key related commands",
Long: `Manage SSH private keys for server authentication - list, add, and remove keys.`,
}
// Add subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewCreateCommand())
cmd.AddCommand(NewDeleteCommand())
return cmd
}
+77
View File
@@ -0,0 +1,77 @@
package project
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// EnvironmentRow represents an environment for display
type EnvironmentRow struct {
UUID string `json:"environment_uuid"`
EnvironmentName string `json:"environment_name"`
}
// NewGetCommand returns the get project command
func NewGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <uuid>",
Short: "Get a project by uuid",
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
projectSvc := service.NewProjectService(client)
project, err := projectSvc.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get project: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// For JSON/pretty formats, return the full project structure
if format != output.FormatTable {
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(project)
}
// For table format, expand environments into separate rows
var rows []EnvironmentRow
// If the project has environments, expand them
if project.Environments != nil && len(project.Environments) > 0 {
for _, env := range project.Environments {
rows = append(rows, EnvironmentRow{
UUID: env.UUID,
EnvironmentName: env.Name,
})
}
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(rows)
},
}
}
+77
View File
@@ -0,0 +1,77 @@
package project
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// ListRow represents a project for list display (without environments)
type ListRow struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Description string `json:"description"`
}
// NewListCommand returns the list projects command
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all projects",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
projectSvc := service.NewProjectService(client)
projects, err := projectSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list projects: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// For JSON/pretty formats, return the full project structure
if format != output.FormatTable {
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(projects)
}
// For table format, convert to simplified rows without environments
var rows []ListRow
for _, p := range projects {
desc := ""
if p.Description != nil {
desc = *p.Description
}
rows = append(rows, ListRow{
UUID: p.UUID,
Name: p.Name,
Description: desc,
})
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(rows)
},
}
}
+19
View File
@@ -0,0 +1,19 @@
package project
import "github.com/spf13/cobra"
// NewProjectCommand creates the project parent command with all subcommands
func NewProjectCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "project",
Aliases: []string{"projects"},
Short: "Project related commands",
Long: `Manage Coolify projects - list all projects or get details about a specific project.`,
}
// Add all project subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
return cmd
}
-160
View File
@@ -1,160 +0,0 @@
package cmd
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// EnvironmentRow represents an environment for display
type EnvironmentRow struct {
UUID string `json:"environment_uuid"`
EnvironmentName string `json:"environment_name"`
}
// ProjectListRow represents a project for list display (without environments)
type ProjectListRow struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Description string `json:"description"`
}
var projectsCmd = &cobra.Command{
Use: "project",
Aliases: []string{"projects"},
Short: "Project related commands",
Long: `Manage Coolify projects - list all projects or get details about a specific project.`,
}
var listProjectsCmd = &cobra.Command{
Use: "list",
Short: "List all projects",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
projectSvc := service.NewProjectService(client)
projects, err := projectSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list projects: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// For JSON/pretty formats, return the full project structure
if format != output.FormatTable {
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(projects)
}
// For table format, convert to simplified rows without environments
var rows []ProjectListRow
for _, p := range projects {
desc := ""
if p.Description != nil {
desc = *p.Description
}
rows = append(rows, ProjectListRow{
UUID: p.UUID,
Name: p.Name,
Description: desc,
})
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(rows)
},
}
var oneProjectCmd = &cobra.Command{
Use: "get [uuid]",
Short: "Get a project by uuid",
Args: exactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
projectSvc := service.NewProjectService(client)
project, err := projectSvc.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get project: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// For JSON/pretty formats, return the full project structure
if format != output.FormatTable {
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(project)
}
// For table format, expand environments into separate rows
rows := expandProjectEnvironments(project)
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(rows)
},
}
// expandProjectEnvironments creates environment rows for display
func expandProjectEnvironments(project *models.Project) []EnvironmentRow {
var rows []EnvironmentRow
// If no environments, return empty list
if len(project.Environments) == 0 {
return rows
}
// Create one row per environment with just UUID and Name
for _, env := range project.Environments {
rows = append(rows, EnvironmentRow{
UUID: env.UUID,
EnvironmentName: env.Name,
})
}
return rows
}
func init() {
rootCmd.AddCommand(projectsCmd)
projectsCmd.AddCommand(listProjectsCmd)
projectsCmd.AddCommand(oneProjectCmd)
}
-53
View File
@@ -1,53 +0,0 @@
package cmd
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
var resourcesCmd = &cobra.Command{
Use: "resource",
Aliases: []string{"resources"},
Short: "Resource related commands",
Long: `List all resources (applications, services, databases) across your Coolify instance.`,
}
var listResourcesCmd = &cobra.Command{
Use: "list",
Short: "List all resources",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
resourceSvc := service.NewResourceService(client)
resources, err := resourceSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list resources: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(resources)
},
}
func init() {
rootCmd.AddCommand(resourcesCmd)
resourcesCmd.AddCommand(listResourcesCmd)
}
+45
View File
@@ -0,0 +1,45 @@
package resources
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewListCommand returns the list projects command
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all resources",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
resourceSvc := service.NewResourceService(client)
resources, err := resourceSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list resources: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(resources)
},
}
}
+20
View File
@@ -0,0 +1,20 @@
package resources
import (
"github.com/spf13/cobra"
)
// NewResourceCommand creates the resource parent command with all subcommands
func NewResourceCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "resource",
Aliases: []string{"resources"},
Short: "Resource related commands",
Long: `List all resources (applications, services, databases) in Coolify.`,
}
// Add all resource subcommands
cmd.AddCommand(NewListCommand())
return cmd
}
+62 -211
View File
@@ -1,239 +1,61 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"sort"
"text/tabwriter"
"time"
"github.com/coollabsio/coolify-cli/internal/api"
"github.com/coollabsio/coolify-cli/cmd/application"
"github.com/coollabsio/coolify-cli/cmd/completion"
configcmd "github.com/coollabsio/coolify-cli/cmd/config"
"github.com/coollabsio/coolify-cli/cmd/context"
"github.com/coollabsio/coolify-cli/cmd/database"
"github.com/coollabsio/coolify-cli/cmd/deployment"
"github.com/coollabsio/coolify-cli/cmd/github"
"github.com/coollabsio/coolify-cli/cmd/privatekeys"
"github.com/coollabsio/coolify-cli/cmd/project"
"github.com/coollabsio/coolify-cli/cmd/resources"
"github.com/coollabsio/coolify-cli/cmd/server"
"github.com/coollabsio/coolify-cli/cmd/service"
"github.com/coollabsio/coolify-cli/cmd/teams"
"github.com/coollabsio/coolify-cli/cmd/update"
cliversion "github.com/coollabsio/coolify-cli/cmd/version"
"github.com/coollabsio/coolify-cli/internal/config"
"github.com/coollabsio/coolify-cli/internal/version"
compareVersion "github.com/hashicorp/go-version"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// CliVersion is the CLI version
var CliVersion = "1.0.0"
// CheckInterval for version checking
var CheckInterval = 10 * time.Minute
// SensitiveInformationOverlay is the string used to hide sensitive data
var SensitiveInformationOverlay = "********"
// Legacy global variables - kept for backward compatibility during migration
// TODO: Remove these once all commands are refactored
var (
Version string
Name string
Fqdn string
Token string
ContextName string
Debug bool
ShowSensitive bool
Format string
JsonMode bool
PrettyMode bool
Version string
Name string
Fqdn string
Token string
ContextName string
Debug bool
ShowSensitive bool
Format string
JsonMode bool
PrettyMode bool
SetDefaultInstance bool
w = tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug)
Instance http.Client
w = tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug)
)
// Tag represents a git tag for version checking
type Tag struct {
Ref string `json:"ref"`
}
var rootCmd = &cobra.Command{
Use: "coolify",
Short: "Coolify CLI",
Long: `A CLI tool to interact with Coolify API.`,
SilenceUsage: true, // Don't show usage on errors
Use: "coolify",
Short: "Coolify CLI",
Long: `A CLI tool to interact with Coolify API.`,
SilenceUsage: true, // Don't show usage on errors
SilenceErrors: false, // Still print errors
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
// getAPIClient creates an API client from command flags or config
func getAPIClient(cmd *cobra.Command) (*api.Client, error) {
// Get flags
token, _ := cmd.Flags().GetString("token")
contextName, _ := cmd.Flags().GetString("context")
debug, _ := cmd.Flags().GetBool("debug")
// Load config to get instance details
cfg, err := config.Load()
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
var instance *config.Instance
// Use context if specified, otherwise use default
if contextName != "" {
instance, err = cfg.GetInstance(contextName)
if err != nil {
return nil, fmt.Errorf("context '%s' not found: %w", contextName, err)
}
} else {
instance, err = cfg.GetDefault()
if err != nil {
return nil, fmt.Errorf("no default instance configured: %w", err)
}
}
// Get FQDN from instance
fqdn := instance.FQDN
// Use token from flag if provided, otherwise use instance token
if token == "" {
token = instance.Token
}
// Create client
client := api.NewClient(fqdn, token, api.WithDebug(debug))
// Set legacy global variables for backward compatibility
Fqdn = fqdn
Token = token
Debug = debug
return client, nil
}
// exactArgs returns a validator that ensures exactly n arguments are provided with a helpful error message
func exactArgs(n int, usage string) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) != n {
if n == 1 {
return fmt.Errorf("missing required argument: %s\n\nUsage: %s", usage, cmd.UseLine())
}
return fmt.Errorf("expected %d argument(s), got %d\n\nUsage: %s", n, len(args), cmd.UseLine())
}
return nil
}
}
// minArgs returns a validator that ensures at least n arguments are provided with a helpful error message
func minArgs(n int, usage string) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) < n {
return fmt.Errorf("missing required arguments: %s\n\nUsage: %s", usage, cmd.UseLine())
}
return nil
}
}
// parseInt parses a string to int with better error message
func parseInt(s string) (int, error) {
var result int
_, err := fmt.Sscanf(s, "%d", &result)
if err != nil {
return 0, fmt.Errorf("'%s' is not a valid integer", s)
}
return result, nil
}
// splitOwnerRepo splits owner/repo string into parts
func splitOwnerRepo(s string) []string {
parts := make([]string, 0, 2)
var current string
for _, char := range s {
if char == '/' {
if current != "" {
parts = append(parts, current)
current = ""
}
} else {
current += string(char)
}
}
if current != "" {
parts = append(parts, current)
}
return parts
}
// CheckLatestVersionOfCli checks for CLI updates
func CheckLatestVersionOfCli() (string, error) {
lastCheck := viper.GetString("lastupdatechecktime")
if lastCheck != "" {
lastCheckTime, err := time.Parse(time.RFC3339, lastCheck)
if err == nil && lastCheckTime.Add(CheckInterval).After(time.Now()) {
if Debug {
log.Println("Skipping update check. Last check was less than 10 minutes ago.")
}
return CliVersion, nil
}
}
// Update check time
viper.Set("lastupdatechecktime", time.Now().Format(time.RFC3339))
viper.WriteConfig()
url := "https://api.github.com/repos/coollabsio/coolify-cli/git/refs/tags"
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("%d - Failed to fetch data from %s. Error: %s", resp.StatusCode, url, string(body))
}
var tags []Tag
if err := json.Unmarshal(body, &tags); err != nil {
return "", err
}
versionsRaw := make([]string, 0, len(tags))
for _, tag := range tags {
versionStr := tag.Ref[10:]
versionsRaw = append(versionsRaw, versionStr)
}
versions := make([]*compareVersion.Version, len(versionsRaw))
for i, raw := range versionsRaw {
v, err := compareVersion.NewVersion(raw)
if err != nil {
return "", err
}
versions[i] = v
}
sort.Sort(compareVersion.Collection(versions))
latestVersion := versions[len(versions)-1]
// Compare versions properly using semantic versioning
currentVersion, err := compareVersion.NewVersion(CliVersion)
if err != nil {
return latestVersion.String(), err
}
if latestVersion.GreaterThan(currentVersion) {
fmt.Printf("There is a new version of Coolify CLI available.\nPlease update with 'coolify update'.\n\n")
}
return latestVersion.String(), nil
}
// Execute runs the root command
func Execute() {
err := rootCmd.Execute()
@@ -243,6 +65,17 @@ func Execute() {
}
func init() {
rootCmd = &cobra.Command{
Use: "coolify",
Short: "Coolify CLI",
Long: fmt.Sprintf("A CLI tool to interact with Coolify API.\nVersion: %s", version.CliVersion),
SilenceUsage: true, // Don't show usage on errors
SilenceErrors: false, // Still print errors
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVarP(&Token, "token", "", "", "Token for authentication (override context token)")
@@ -251,6 +84,24 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&Format, "format", "", "table", "Format output (table|json|pretty)")
rootCmd.PersistentFlags().BoolVarP(&ShowSensitive, "show-sensitive", "s", false, "Show sensitive information")
rootCmd.PersistentFlags().BoolVarP(&Debug, "debug", "", false, "Debug mode")
// Register all subcommands
rootCmd.AddCommand(application.NewAppCommand())
rootCmd.AddCommand(completion.NewCompletionsCommand())
rootCmd.AddCommand(configcmd.NewConfigCommand())
rootCmd.AddCommand(context.NewContextCommand())
rootCmd.AddCommand(database.NewDatabaseCommand())
rootCmd.AddCommand(deployment.NewDeploymentCommand())
rootCmd.AddCommand(github.NewGitHubCommand())
rootCmd.AddCommand(privatekeys.NewPrivateKeysCommand())
rootCmd.AddCommand(project.NewProjectCommand())
rootCmd.AddCommand(resources.NewResourceCommand())
rootCmd.AddCommand(server.NewServerCommand())
rootCmd.AddCommand(service.NewServiceCommand())
rootCmd.AddCommand(teams.NewTeamsCommand())
rootCmd.AddCommand(update.NewUpdateCommand())
rootCmd.AddCommand(cliversion.NewVersionCommand())
rootCmd.AddCommand(NewDocsCommand())
}
func initConfig() {
@@ -291,7 +142,7 @@ func initConfig() {
// This allows --instance flag to work correctly
// Check for updates
latestVersionStr, err := CheckLatestVersionOfCli()
latestVersionStr, err := version.CheckLatestVersionOfCli(Debug)
if err != nil {
if Debug {
log.Println("Failed to check for updates:", err)
@@ -302,7 +153,7 @@ func initConfig() {
if latestVersionStr != "" {
latestVersion, err := compareVersion.NewVersion(latestVersionStr)
if err == nil {
currentVersion, err := compareVersion.NewVersion(CliVersion)
currentVersion, err := compareVersion.NewVersion(version.CliVersion)
if err == nil && latestVersion.GreaterThan(currentVersion) {
if Debug {
log.Printf("New version of Coolify CLI is available: %s\n", latestVersionStr)
+68
View File
@@ -0,0 +1,68 @@
package server
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewAddCommand creates the add command
func NewAddCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "add <server_name> <ip_address> <private_key_uuid>",
Args: cli.ExactArgs(3, "<server_name> <ip_address> <private_key_uuid>"),
Short: "Add a server",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Parse arguments and flags
name := args[0]
ip := args[1]
privateKeyUuid := args[2]
port, _ := cmd.Flags().GetInt("port")
user, _ := cmd.Flags().GetString("user")
validate, _ := cmd.Flags().GetBool("validate")
// Create request
req := models.ServerCreateRequest{
Name: name,
IP: ip,
Port: port,
User: user,
PrivateKeyUUID: privateKeyUuid,
InstantValidate: validate,
}
// Use service layer
serverSvc := service.NewServerService(client)
response, err := serverSvc.Create(ctx, req)
if err != nil {
return fmt.Errorf("failed to create server: %w", err)
}
if validate {
fmt.Printf("Server added successfully with uuid %s\n", response.UUID)
} else {
fmt.Printf("Server added successfully with uuid %s. Server is not validated. Use 'servers validate %s' to validate the server.\n", response.UUID, response.UUID)
}
return nil
},
}
cmd.Flags().IntP("port", "p", 22, "Port")
cmd.Flags().StringP("user", "u", "root", "User")
cmd.Flags().Bool("validate", false, "Validate the server")
return cmd
}
+63
View File
@@ -0,0 +1,63 @@
package server
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewGetCommand creates the get command
func NewGetDomainsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "domains <uuid>",
Aliases: []string{"domain"},
Args: cli.ExactArgs(1, "<uuid>"),
Short: "Get server domains by uuid",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Use service layer
serverSvc := service.NewServerService(client)
uuid := args[0]
// Get format flags
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
domains, err := serverSvc.GetDomains(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get server domains: %w", err)
}
// Use output formatter
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
if err := formatter.Format(domains); err != nil {
return err
}
if !showSensitive && format == output.FormatTable {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
return cmd
}
+75
View File
@@ -0,0 +1,75 @@
package server
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewGetCommand creates the get command
func NewGetCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "get <uuid>",
Args: cli.ExactArgs(1, "<uuid>"),
Short: "Get server details by uuid",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Use service layer
serverSvc := service.NewServerService(client)
uuid := args[0]
// Get format flags
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
withResources, _ := cmd.Flags().GetBool("resources")
var data interface{}
if withResources {
resources, err := serverSvc.GetResources(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get server resources: %w", err)
}
data = resources.Resources
} else {
server, err := serverSvc.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get server: %w", err)
}
data = server
}
// Use output formatter
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
if err := formatter.Format(data); err != nil {
return err
}
if !showSensitive && format == output.FormatTable && !withResources {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
cmd.Flags().Bool("resources", false, "With resources")
return cmd
}
+56
View File
@@ -0,0 +1,56 @@
package server
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewListCommand creates the list command
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all servers",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Use service layer
serverSvc := service.NewServerService(client)
servers, err := serverSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list servers: %w", err)
}
// Use output formatter
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
if err := formatter.Format(servers); err != nil {
return err
}
if !showSensitive && format == output.FormatTable {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
}
+39
View File
@@ -0,0 +1,39 @@
package server
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewRemoveCommand creates the remove command
func NewRemoveCommand() *cobra.Command {
return &cobra.Command{
Use: "remove <uuid>",
Args: cli.ExactArgs(1, "<uuid>"),
Short: "Remove a server",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Use service layer
serverSvc := service.NewServerService(client)
uuid := args[0]
if err := serverSvc.Delete(ctx, uuid); err != nil {
return fmt.Errorf("failed to delete server: %w", err)
}
fmt.Printf("Server %s deleted successfully\n", uuid)
return nil
},
}
}
+25
View File
@@ -0,0 +1,25 @@
package server
import (
"github.com/spf13/cobra"
)
// NewServerCommand creates the server parent command
func NewServerCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "server",
Aliases: []string{"servers"},
Short: "Server related commands",
Long: `Manage Coolify servers - list, get details, add new servers, validate connections, and remove servers.`,
}
// Add subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewGetDomainsCommand())
cmd.AddCommand(NewAddCommand())
cmd.AddCommand(NewRemoveCommand())
cmd.AddCommand(NewValidateCommand())
return cmd
}
+45
View File
@@ -0,0 +1,45 @@
package server
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewValidateCommand creates the validate command
func NewValidateCommand() *cobra.Command {
return &cobra.Command{
Use: "validate <uuid>",
Args: cli.ExactArgs(1, "<uuid>"),
Short: "Validate a server",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Use service layer
serverSvc := service.NewServerService(client)
uuid := args[0]
response, err := serverSvc.Validate(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to validate server: %w", err)
}
if response.Message != "" {
fmt.Println(response.Message)
} else {
fmt.Printf("Server %s validated successfully\n", uuid)
}
return nil
},
}
}
-247
View File
@@ -1,247 +0,0 @@
package cmd
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
var WithResources bool
var serversCmd = &cobra.Command{
Use: "server",
Aliases: []string{"servers"},
Short: "Server related commands",
Long: `Manage Coolify servers - list, get details, add new servers, validate connections, and remove servers.`,
}
var listServersCmd = &cobra.Command{
Use: "list",
Short: "List all servers",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Check API version
version, err := client.GetVersion(ctx)
if err != nil {
return fmt.Errorf("failed to get API version: %w", err)
}
Version = version
// Use service layer
serverSvc := service.NewServerService(client)
servers, err := serverSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list servers: %w", err)
}
// Use output formatter
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
if err := formatter.Format(servers); err != nil {
return err
}
if !showSensitive && format == output.FormatTable {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
var oneServerCmd = &cobra.Command{
Use: "get [uuid]",
Args: exactArgs(1, "<uuid>"),
Short: "Get server details by uuid",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Use service layer
serverSvc := service.NewServerService(client)
uuid := args[0]
// Get format flags
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
var data interface{}
if WithResources {
resources, err := serverSvc.GetResources(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get server resources: %w", err)
}
data = resources.Resources
} else {
server, err := serverSvc.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get server: %w", err)
}
data = server
}
// Use output formatter
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
if err := formatter.Format(data); err != nil {
return err
}
if !showSensitive && format == output.FormatTable && !WithResources {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
var removeServerCmd = &cobra.Command{
Use: "remove [uuid]",
Args: exactArgs(1, "<uuid>"),
Short: "Remove a server",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Use service layer
serverSvc := service.NewServerService(client)
uuid := args[0]
if err := serverSvc.Delete(ctx, uuid); err != nil {
return fmt.Errorf("failed to delete server: %w", err)
}
fmt.Printf("Server %s deleted successfully\n", uuid)
return nil
},
}
var addServerCmd = &cobra.Command{
Use: "add [name] [ip] [private_key_uuid]",
Args: exactArgs(3, "<uuid1> <uuid2> <uuid3>"),
Short: "Add a server",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Parse arguments and flags
name := args[0]
ip := args[1]
privateKeyUuid := args[2]
port, _ := cmd.Flags().GetInt("port")
user, _ := cmd.Flags().GetString("user")
validate, _ := cmd.Flags().GetBool("validate")
// Create request
req := models.ServerCreateRequest{
Name: name,
IP: ip,
Port: port,
User: user,
PrivateKeyUUID: privateKeyUuid,
InstantValidate: validate,
}
// Use service layer
serverSvc := service.NewServerService(client)
response, err := serverSvc.Create(ctx, req)
if err != nil {
return fmt.Errorf("failed to create server: %w", err)
}
if validate {
fmt.Printf("Server added successfully with uuid %s\n", response.UUID)
} else {
fmt.Printf("Server added successfully with uuid %s. Server is not validated. Use 'servers validate %s' to validate the server.\n", response.UUID, response.UUID)
}
return nil
},
}
var validateServerCmd = &cobra.Command{
Use: "validate [uuid]",
Args: exactArgs(1, "<uuid>"),
Short: "Validate a server",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Get API client
client, err := getAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Use service layer
serverSvc := service.NewServerService(client)
uuid := args[0]
response, err := serverSvc.Validate(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to validate server: %w", err)
}
if response.Message != "" {
fmt.Println(response.Message)
} else {
fmt.Printf("Server %s validated successfully\n", uuid)
}
return nil
},
}
func init() {
// Note: format and show-sensitive flags are inherited from rootCmd.PersistentFlags()
oneServerCmd.Flags().BoolVarP(&WithResources, "resources", "", false, "With resources")
rootCmd.AddCommand(serversCmd)
serversCmd.AddCommand(listServersCmd)
serversCmd.AddCommand(oneServerCmd)
addServerCmd.Flags().IntP("port", "p", 22, "Port")
addServerCmd.Flags().StringP("user", "u", "root", "User")
addServerCmd.Flags().BoolP("validate", "", false, "Validate the server")
serversCmd.AddCommand(addServerCmd)
serversCmd.AddCommand(validateServerCmd)
serversCmd.AddCommand(removeServerCmd)
}
+64
View File
@@ -0,0 +1,64 @@
package service
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewDeleteCommand deletes a service
func NewDeleteCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete <uuid>",
Short: "Delete a service",
Long: `Delete a service and optionally clean up its configurations, volumes, and networks.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
deleteConfigurations, _ := cmd.Flags().GetBool("delete-configurations")
deleteVolumes, _ := cmd.Flags().GetBool("delete-volumes")
dockerCleanup, _ := cmd.Flags().GetBool("docker-cleanup")
deleteConnectedNetworks, _ := cmd.Flags().GetBool("delete-connected-networks")
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to delete this service? (yes/no): ")
fmt.Scanln(&response)
if response != "yes" && response != "y" {
fmt.Println("Delete cancelled.")
return nil
}
}
serviceSvc := service.NewServiceService(client)
err = serviceSvc.Delete(ctx, uuid, deleteConfigurations, deleteVolumes, dockerCleanup, deleteConnectedNetworks)
if err != nil {
return fmt.Errorf("failed to delete service: %w", err)
}
fmt.Println("Service deletion request queued.")
return nil
},
}
cmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
cmd.Flags().Bool("delete-configurations", true, "Delete configurations")
cmd.Flags().Bool("delete-volumes", true, "Delete volumes")
cmd.Flags().Bool("docker-cleanup", true, "Run docker cleanup")
cmd.Flags().Bool("delete-connected-networks", true, "Delete connected networks")
return cmd
}
+80
View File
@@ -0,0 +1,80 @@
package env
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewCreateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create <service_uuid>",
Short: "Create an environment variable for a service",
Long: `Create a new environment variable for a specific service. Use --key and --value flags to specify the variable.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
key, _ := cmd.Flags().GetString("key")
value, _ := cmd.Flags().GetString("value")
isBuildTime, _ := cmd.Flags().GetBool("build-time")
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
if key == "" {
return fmt.Errorf("--key is required")
}
if value == "" {
return fmt.Errorf("--value is required")
}
req := &models.EnvironmentVariableCreateRequest{
Key: key,
Value: value,
}
// Only set flags if they were explicitly provided
if cmd.Flags().Changed("build-time") {
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("is-multiline") {
req.IsMultiline = &isMultiline
}
serviceSvc := service.NewServiceService(client)
env, err := serviceSvc.CreateEnv(ctx, uuid, req)
if err != nil {
return fmt.Errorf("failed to create environment variable: %w", err)
}
fmt.Printf("Environment variable '%s' created successfully.\n", env.Key)
return nil
},
}
cmd.Flags().String("key", "", "Environment variable key (required)")
cmd.Flags().String("value", "", "Environment variable value (required)")
cmd.Flags().Bool("build-time", false, "Available at build time")
cmd.Flags().Bool("preview", false, "Available in preview deployments")
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
return cmd
}
+56
View File
@@ -0,0 +1,56 @@
package env
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewDeleteCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete <service_uuid> <env_uuid>",
Short: "Delete an environment variable",
Long: `Delete an environment variable from a service. First UUID is the service, second is the specific environment variable to delete.`,
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
serviceUUID := args[0]
envUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to delete this environment variable? (yes/no): ")
fmt.Scanln(&response)
if response != "yes" && response != "y" {
fmt.Println("Delete cancelled.")
return nil
}
}
serviceSvc := service.NewServiceService(client)
err = serviceSvc.DeleteEnv(ctx, serviceUUID, envUUID)
if err != nil {
return fmt.Errorf("failed to delete environment variable: %w", err)
}
fmt.Println("Environment variable deleted successfully.")
return nil
},
}
cmd.Flags().Bool("force", false, "Skip confirmation prompt")
return cmd
}
+4
View File
@@ -0,0 +1,4 @@
// Package env contains service environment variable commands
package env
// This package handles environment variable management for services
+57
View File
@@ -0,0 +1,57 @@
package env
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <service_uuid> <env_uuid_or_key>",
Short: "Get environment variable details",
Long: `Get detailed information about a specific environment variable. First UUID is the service, second is the environment variable UUID or key name.`,
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
serviceUUID := args[0]
envUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
serviceSvc := service.NewServiceService(client)
env, err := serviceSvc.GetEnv(ctx, serviceUUID, envUUID)
if err != nil {
return fmt.Errorf("failed to get environment variable: %w", err)
}
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// Mask sensitive value unless --show-sensitive is used
if !showSensitive {
env.Value = "********"
if env.RealValue != nil {
masked := "********"
env.RealValue = &masked
}
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(env)
},
}
}
+58
View File
@@ -0,0 +1,58 @@
package env
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list <service_uuid>",
Short: "List all environment variables for a service",
Long: `List all environment variables for a specific service.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
serviceSvc := service.NewServiceService(client)
envs, err := serviceSvc.ListEnvs(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to list environment variables: %w", err)
}
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// Mask sensitive values unless --show-sensitive is used
if !showSensitive {
for i := range envs {
envs[i].Value = "********"
if envs[i].RealValue != nil {
masked := "********"
envs[i].RealValue = &masked
}
}
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(envs)
},
}
}

Some files were not shown because too many files have changed in this diff Show More