37 Commits

Author SHA1 Message Date
Andras Bacsai bd65345df8 Merge pull request #51 from YaRissi/fix/service-update-env
fix: update service env command
2026-03-19 22:03:20 +01:00
Andras Bacsai c94e147639 feat(env): enforce minimum version requirement for updates 2026-03-19 22:00:40 +01:00
Andras Bacsai 0ea34284ef fix(env): require key and value flags for updating variables
- Changed app and service env update commands to accept only the resource UUID instead of separate env UUID argument
- Made --key and --value required flags for identifying and updating environment variables
- Removed UUID field from EnvironmentVariableUpdateRequest model
- Updated validation to explicitly require --key and --value instead of "at least one field"
- Changed ServiceEnvBulkUpdateResponse from struct with message to slice of ServiceEnvironmentVariable
- Updated BulkUpdateEnvs return type from pointer to non-pointer
- Updated tests and documentation to reflect new command interface
2026-03-19 21:57:11 +01:00
YaRissi a93872ee16 fix lint 2025-12-19 18:49:13 +01:00
YaRissi 1bc1a601a8 fix update env command 2025-12-19 18:42:42 +01:00
github-actions[bot] 4ad94e2d65 chore: bump version to v1.4.0 2025-12-12 13:04:47 +00:00
Andras Bacsai faa8186301 Merge pull request #47 from coollabsio/project-app-create
feat: add create commands for applications, projects, and services
2025-12-12 14:02:09 +01:00
Andras Bacsai 1eba511544 fix: use assert instead of require in HTTP handlers
Replace require.NoError with assert.NoError inside HTTP handler
functions to fix testifylint go-require violations. Using require
in HTTP handlers can cause unpredictable test behavior since
t.FailNow() only exits the current goroutine (the handler), not
the main test goroutine.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 10:21:33 +01:00
Andras Bacsai 541f633edc feat: add create commands for applications, projects, and services
Add comprehensive create functionality for three main resource types:
- Applications: public, private (GitHub App & deploy key), Dockerfile, Docker image
- Projects: simple project creation with optional description
- Services: one-click service deployment with 80+ service types

Includes full service layer implementation with 15+ test cases covering success and error scenarios. Also fixed EnvironmentUUID handling in service creation requests.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 18:41:31 +01:00
github-actions[bot] 0f23b029f0 chore: bump version to v1.3.0 2025-12-05 12:43:09 +00:00
github-actions[bot] 780b3674c7 chore: bump version to v1.2 2025-12-05 12:38:45 +00:00
Andras Bacsai 77adbfaebc Merge pull request #46 from coollabsio/update-check-every-cmd
Check for CLI updates on every command
2025-12-05 13:35:52 +01:00
Andras Bacsai 9215fd537e feat: check for CLI updates on every command
Remove the 10-minute check interval so update notifications appear on every command execution. Silent error handling prevents network issues from interrupting commands. Updated message format is more concise and shows the available version.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 13:35:05 +01:00
github-actions[bot] 26c0925854 chore: bump version to v1.1 2025-12-05 12:04:57 +00:00
Andras Bacsai 1f1b187ed2 Merge pull request #45 from coollabsio/add-runtime-env-flag
Add runtime env flag and improve service env handling
2025-12-05 13:01:46 +01:00
Andras Bacsai 4af598c213 fix: use is_buildtime JSON tag to match Coolify API response
The Coolify API returns `is_buildtime` (without underscore between
build and time) in responses. Updated service tests to use the correct
field name.

Also simplified application env get command by removing preview filter.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 12:58:18 +01:00
Andras Bacsai 6ca3b700ce fix: resolve lint errors for stuttering type names
Move ServiceBulkUpdateEnvsRequest and ServiceBulkUpdateEnvsResponse
to models package as ServiceEnvBulkUpdateRequest/Response to avoid
the "type name stutters" lint error from revive.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 12:00:41 +01:00
Andras Bacsai 8cf0b71ebf feat: add runtime env flag and improve service env handling
- Add --runtime flag to all env commands (create, update, sync) for both apps and services
- Make --runtime and --build-time flags default to true
- Remove is_preview field from service environment variables (services don't have preview)
- Create ServiceEnvironmentVariable model without preview support
- Wire up service env commands in service.go
- Add --preview filter option to app env list and get commands
- Add --all flag to app env list to show all variables (non-preview first)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 11:54:22 +01:00
github-actions[bot] 99a40bfa1d chore: bump version to v1.0.5 2025-11-27 08:20:33 +00:00
Andras Bacsai 188834fd6d Merge pull request #39 from YaRissi/fix/version
fix: update  release workflow
2025-11-27 09:17:39 +01:00
Andras Bacsai f9c3b9869a Merge pull request #43 from coollabsio/dependabot/go_modules/golang.org/x/crypto-0.45.0
chore(deps): bump golang.org/x/crypto from 0.43.0 to 0.45.0
2025-11-27 09:16:36 +01:00
dependabot[bot] 6044a2107e chore(deps): bump golang.org/x/crypto from 0.43.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.43.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.43.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 08:16:04 +00:00
Andras Bacsai 2f9dc6e8d7 Merge pull request #42 from coollabsio/fix-env-is-build-time-value
Fix is_buildtime JSON tag and add is_runtime, is_shared fields
2025-11-27 09:15:01 +01:00
Andras Bacsai 1e741309cb fix: correct is_buildtime JSON tag and add is_runtime, is_shared fields
Fixed critical bug where is_buildtime field was not unmarshaling from API
responses due to JSON tag mismatch (was expecting 'is_build_time' with
underscore but API returns 'is_buildtime' without underscore). Also added
missing is_runtime and is_shared fields that are present in API responses.

Added comprehensive tests for EnvironmentVariable model and service layer
to ensure proper marshaling/unmarshaling of all fields. Achieved 100%
coverage for EnvironmentVariable struct.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 09:11:35 +01:00
YaRissi 51f759c38f fix: improve error handling in Stop-WithError function and update git tag push command 2025-11-20 02:34:22 +01:00
Andras Bacsai 51e9ec5ec8 Update install.sh 2025-11-18 23:25:17 +01:00
Andras Bacsai e071fd81d4 Merge branch 'v4.x' into fix/version 2025-11-18 23:19:32 +01:00
Andras Bacsai cb0bbfc5cb Merge pull request #41 from ncryptedV1/fix/install-script-release-download-link
fix: leading 'v' for release filename of install script
2025-11-18 23:18:53 +01:00
ncryptedV1 87b6b8fdf7 fix: leading 'v' for release filename of install script 2025-11-18 17:24:25 +01:00
YaRissi 3dbe2507f4 fix deprecated builds tag 2025-11-16 16:39:08 +01:00
YaRissi 1e82217a50 feat: update installation instructions for Windows and add PowerShell script 2025-11-16 16:29:27 +01:00
YaRissi 6f33fa00f1 fix: readd version in binary name 2025-11-16 16:06:03 +01:00
YaRissi d7841b3b5a chore: readd version in binaryname 2025-11-16 15:59:41 +01:00
YaRissi 234f6e9ed6 fix: update binary name in installation script 2025-11-10 21:45:31 +01:00
YaRissi fa86ceb5cc fix: update filename format in download function for consistency 2025-11-10 21:37:22 +01:00
YaRissi 646bf9de36 fix: update version tagging logic in release workflow 2025-11-10 21:07:02 +01:00
github-actions[bot] be29a6e05d chore: bump version to v1.0.4 2025-11-10 13:24:40 +00:00
40 changed files with 2980 additions and 159 deletions
+5
View File
@@ -49,3 +49,8 @@ jobs:
git add internal/version/checker.go
git commit -m "chore: bump version to $TAG"
git push origin v4.x
# Move the tag to point to the new commit with updated version
git tag -d "$TAG" || true
git tag "$TAG"
git push origin "refs/tags/$TAG" --force
+15 -1
View File
@@ -22,4 +22,18 @@ builds:
- amd64
- arm64
env:
- CGO_ENABLED=0
- CGO_ENABLED=0
checksum:
name_template: checksums.txt
algorithm: sha256
archives:
- id: coolify-archive
ids:
- coolify
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
format_overrides:
- goos: windows
formats: [zip]
+1
View File
@@ -9,6 +9,7 @@ This is a CLI tool for interacting with the Coolify API, built with Go using the
### API Specification
This CLI is a client for the Coolify API. The API specification is defined in the OpenAPI schema:
- **Source**: https://github.com/coollabsio/coolify/blob/v4.x/openapi.json
- **Raw JSON**: https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/v4.x/openapi.json
- **Base Path**: `/api/v1/`
- **Authentication**: Bearer token (API tokens from Coolify dashboard at `/security/api-tokens`)
+35 -2
View File
@@ -4,12 +4,32 @@
### Install script (recommended)
#### Linux/macOS
```bash
curl -fsSL https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.sh | bash
```
It will install the CLI in `/usr/local/bin/coolify` and the configuration file in `~/.config/coolify/config.json`
#### Windows (PowerShell)
```powershell
irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
```
It will install the CLI in `%ProgramFiles%\Coolify\coolify.exe` and the configuration file in `%USERPROFILE%\.config\coolify\config.json`
For user installation (no admin rights required):
```powershell
$env:COOLIFY_USER_INSTALL=1; irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
```
For a specific version:
```powershell
$env:COOLIFY_VERSION='v1.0.0'; irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
```
### Using `go install`
```bash
@@ -129,7 +149,14 @@ Commands can use `server` or `servers` interchangeably.
- `--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 update <app_uuid>` - Update an environment variable
- `--key <key>` - Variable key (required)
- `--value <value>` - Variable value (required)
- `--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
- `--runtime` - Available at runtime
- `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
- `--file <path>` - Path to .env file (required)
@@ -212,7 +239,13 @@ Commands can use `server` or `servers` interchangeably.
- `coolify service env get <service_uuid> <env_uuid_or_key>` - Get a specific environment variable
- `coolify service env create <service_uuid>` - Create a new environment variable
- Same flags as application environment variables
- `coolify service env update <service_uuid> <env_uuid>` - Update an environment variable
- `coolify service env update <service_uuid>` - Update an environment variable
- `--key <key>` - Variable key (required)
- `--value <value>` - Variable value (required)
- `--build-time` - Available at build time
- `--is-literal` - Treat value as literal (don't interpolate variables)
- `--is-multiline` - Value is multiline
- `--runtime` - Available at runtime
- `coolify service env delete <service_uuid> <env_uuid>` - Delete an environment variable
- `coolify service env sync <service_uuid>` - Sync environment variables from a .env file
- `--file <path>` - Path to .env file (required)
+2
View File
@@ -3,6 +3,7 @@ package application
import (
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/cmd/application/create"
"github.com/coollabsio/coolify-cli/cmd/application/env"
)
@@ -18,6 +19,7 @@ func NewAppCommand() *cobra.Command {
// Add main subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(create.NewCreateCommand())
cmd.AddCommand(NewUpdateCommand())
cmd.AddCommand(NewDeleteCommand())
cmd.AddCommand(NewStartCommand())
+38
View File
@@ -0,0 +1,38 @@
package create
import "github.com/spf13/cobra"
// NewCreateCommand creates the create parent command with all subcommands
func NewCreateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Create a new application",
Long: `Create a new application from various sources.
Available source types:
public Create from a public git repository
github Create from a private repository using GitHub App
deploy-key Create from a private repository using SSH deploy key
dockerfile Create from a custom Dockerfile
dockerimage Create from a pre-built Docker image
Examples:
coolify app create public --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--git-repository "https://github.com/user/repo" --git-branch main --build-pack nixpacks --ports-exposes 3000
coolify app create github --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--github-app-uuid <uuid> --git-repository "user/repo" --git-branch main --build-pack nixpacks --ports-exposes 3000
coolify app create dockerimage --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--docker-registry-image-name "nginx:latest" --ports-exposes 80`,
}
// Add all create subcommands
cmd.AddCommand(NewPublicCommand())
cmd.AddCommand(NewGitHubCommand())
cmd.AddCommand(NewDeployKeyCommand())
cmd.AddCommand(NewDockerfileCommand())
cmd.AddCommand(NewDockerImageCommand())
return cmd
}
+152
View File
@@ -0,0 +1,152 @@
package create
import (
"fmt"
"github.com/spf13/cobra"
"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"
)
// NewDeployKeyCommand returns the create deploy-key application command
func NewDeployKeyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "deploy-key",
Short: "Create an application from a private repository using SSH deploy key",
Long: `Create a new application from a private git repository using SSH deploy key authentication.
Use 'coolify privatekeys list' to find your private key UUID.
Examples:
coolify app create deploy-key --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--private-key-uuid <uuid> --git-repository "git@github.com:owner/repo.git" --git-branch main \
--build-pack nixpacks --ports-exposes 3000
coolify app create deploy-key --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--private-key-uuid <uuid> --git-repository "git@gitlab.com:owner/repo.git" --git-branch main \
--build-pack dockerfile --ports-exposes 8080 --instant-deploy`,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
// Get required flags
serverUUID, _ := cmd.Flags().GetString("server-uuid")
projectUUID, _ := cmd.Flags().GetString("project-uuid")
privateKeyUUID, _ := cmd.Flags().GetString("private-key-uuid")
gitRepository, _ := cmd.Flags().GetString("git-repository")
gitBranch, _ := cmd.Flags().GetString("git-branch")
buildPack, _ := cmd.Flags().GetString("build-pack")
portsExposes, _ := cmd.Flags().GetString("ports-exposes")
environmentName, _ := cmd.Flags().GetString("environment-name")
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
// Validate required fields
if serverUUID == "" || projectUUID == "" {
return fmt.Errorf("--server-uuid and --project-uuid are required")
}
if privateKeyUUID == "" {
return fmt.Errorf("--private-key-uuid is required")
}
if gitRepository == "" || gitBranch == "" {
return fmt.Errorf("--git-repository and --git-branch are required")
}
if buildPack == "" || portsExposes == "" {
return fmt.Errorf("--build-pack and --ports-exposes are required")
}
if environmentName == "" && environmentUUID == "" {
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
}
req := &models.ApplicationCreateDeployKeyRequest{
ServerUUID: serverUUID,
ProjectUUID: projectUUID,
PrivateKeyUUID: privateKeyUUID,
GitRepository: gitRepository,
GitBranch: gitBranch,
BuildPack: buildPack,
PortsExposes: portsExposes,
}
if environmentName != "" {
req.EnvironmentName = &environmentName
}
if environmentUUID != "" {
req.EnvironmentUUID = &environmentUUID
}
// Optional fields
setOptionalStringFlag(cmd, "name", &req.Name)
setOptionalStringFlag(cmd, "description", &req.Description)
setOptionalStringFlag(cmd, "domains", &req.Domains)
setOptionalStringFlag(cmd, "git-commit-sha", &req.GitCommitSHA)
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
setOptionalStringFlag(cmd, "build-command", &req.BuildCommand)
setOptionalStringFlag(cmd, "start-command", &req.StartCommand)
setOptionalStringFlag(cmd, "install-command", &req.InstallCommand)
setOptionalStringFlag(cmd, "base-directory", &req.BaseDirectory)
setOptionalStringFlag(cmd, "publish-directory", &req.PublishDirectory)
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
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.CreateDeployKey(ctx, req)
if err != nil {
return fmt.Errorf("failed to create 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)
},
}
// Required 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("private-key-uuid", "", "Private key UUID (required)")
cmd.Flags().String("git-repository", "", "Git repository SSH URL, e.g., 'git@github.com:owner/repo.git' (required)")
cmd.Flags().String("git-branch", "", "Git branch (required)")
cmd.Flags().String("build-pack", "", "Build pack: nixpacks, static, dockerfile, dockercompose (required)")
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '3000' or '3000,8080' (required)")
// Optional flags
cmd.Flags().String("name", "", "Application name")
cmd.Flags().String("description", "", "Application description")
cmd.Flags().String("domains", "", "Domain(s) for the application")
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
cmd.Flags().String("git-commit-sha", "", "Specific commit SHA to deploy")
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
cmd.Flags().String("build-command", "", "Custom build command")
cmd.Flags().String("start-command", "", "Custom start command")
cmd.Flags().String("install-command", "", "Custom install command")
cmd.Flags().String("base-directory", "", "Base directory for the application")
cmd.Flags().String("publish-directory", "", "Publish directory for static builds")
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
cmd.Flags().String("limits-cpus", "", "CPU limit")
cmd.Flags().String("limits-memory", "", "Memory limit")
cmd.Flags().Bool("health-check-enabled", false, "Enable health checks")
cmd.Flags().String("health-check-path", "", "Health check path")
return cmd
}
+120
View File
@@ -0,0 +1,120 @@
package create
import (
"fmt"
"github.com/spf13/cobra"
"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"
)
// NewDockerfileCommand returns the create dockerfile application command
func NewDockerfileCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "dockerfile",
Short: "Create an application from a custom Dockerfile",
Long: `Create a new application from a custom Dockerfile content.
Examples:
coolify app create dockerfile --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--dockerfile "FROM node:18\nWORKDIR /app\nCOPY . .\nRUN npm install\nCMD [\"npm\", \"start\"]"
coolify app create dockerfile --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--dockerfile "$(cat Dockerfile)" --ports-exposes 3000 --instant-deploy`,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
// Get required flags
serverUUID, _ := cmd.Flags().GetString("server-uuid")
projectUUID, _ := cmd.Flags().GetString("project-uuid")
dockerfile, _ := cmd.Flags().GetString("dockerfile")
environmentName, _ := cmd.Flags().GetString("environment-name")
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
// Validate required fields
if serverUUID == "" || projectUUID == "" {
return fmt.Errorf("--server-uuid and --project-uuid are required")
}
if dockerfile == "" {
return fmt.Errorf("--dockerfile is required")
}
if environmentName == "" && environmentUUID == "" {
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
}
req := &models.ApplicationCreateDockerfileRequest{
ServerUUID: serverUUID,
ProjectUUID: projectUUID,
Dockerfile: dockerfile,
}
if environmentName != "" {
req.EnvironmentName = &environmentName
}
if environmentUUID != "" {
req.EnvironmentUUID = &environmentUUID
}
// Optional fields
setOptionalStringFlag(cmd, "name", &req.Name)
setOptionalStringFlag(cmd, "description", &req.Description)
setOptionalStringFlag(cmd, "domains", &req.Domains)
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
setOptionalStringFlag(cmd, "ports-exposes", &req.PortsExposes)
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
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.CreateDockerfile(ctx, req)
if err != nil {
return fmt.Errorf("failed to create 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)
},
}
// Required 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("dockerfile", "", "Dockerfile content (required)")
// Optional flags
cmd.Flags().String("name", "", "Application name")
cmd.Flags().String("description", "", "Application description")
cmd.Flags().String("domains", "", "Domain(s) for the application")
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '3000' or '3000,8080'")
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
cmd.Flags().String("limits-cpus", "", "CPU limit")
cmd.Flags().String("limits-memory", "", "Memory limit")
cmd.Flags().Bool("health-check-enabled", false, "Enable health checks")
cmd.Flags().String("health-check-path", "", "Health check path")
return cmd
}
+127
View File
@@ -0,0 +1,127 @@
package create
import (
"fmt"
"github.com/spf13/cobra"
"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"
)
// NewDockerImageCommand returns the create dockerimage application command
func NewDockerImageCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "dockerimage",
Short: "Create an application from a pre-built Docker image",
Long: `Create a new application from a pre-built Docker image from a registry.
Examples:
coolify app create dockerimage --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--docker-registry-image-name "nginx:latest" --ports-exposes 80
coolify app create dockerimage --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--docker-registry-image-name "ghcr.io/myorg/myapp" --docker-registry-image-tag "v1.0.0" \
--ports-exposes 3000 --domains "myapp.example.com" --instant-deploy`,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
// Get required flags
serverUUID, _ := cmd.Flags().GetString("server-uuid")
projectUUID, _ := cmd.Flags().GetString("project-uuid")
dockerRegistryImageName, _ := cmd.Flags().GetString("docker-registry-image-name")
portsExposes, _ := cmd.Flags().GetString("ports-exposes")
environmentName, _ := cmd.Flags().GetString("environment-name")
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
// Validate required fields
if serverUUID == "" || projectUUID == "" {
return fmt.Errorf("--server-uuid and --project-uuid are required")
}
if dockerRegistryImageName == "" {
return fmt.Errorf("--docker-registry-image-name is required")
}
if portsExposes == "" {
return fmt.Errorf("--ports-exposes is required")
}
if environmentName == "" && environmentUUID == "" {
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
}
req := &models.ApplicationCreateDockerImageRequest{
ServerUUID: serverUUID,
ProjectUUID: projectUUID,
DockerRegistryImageName: dockerRegistryImageName,
PortsExposes: portsExposes,
}
if environmentName != "" {
req.EnvironmentName = &environmentName
}
if environmentUUID != "" {
req.EnvironmentUUID = &environmentUUID
}
// Optional fields
setOptionalStringFlag(cmd, "name", &req.Name)
setOptionalStringFlag(cmd, "description", &req.Description)
setOptionalStringFlag(cmd, "domains", &req.Domains)
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
setOptionalStringFlag(cmd, "docker-registry-image-tag", &req.DockerRegistryImageTag)
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
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.CreateDockerImage(ctx, req)
if err != nil {
return fmt.Errorf("failed to create 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)
},
}
// Required 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("docker-registry-image-name", "", "Docker image name from registry (required)")
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '80' or '80,443' (required)")
// Optional flags
cmd.Flags().String("name", "", "Application name")
cmd.Flags().String("description", "", "Application description")
cmd.Flags().String("domains", "", "Domain(s) for the application")
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
cmd.Flags().String("docker-registry-image-tag", "", "Docker image tag (defaults to 'latest')")
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
cmd.Flags().String("limits-cpus", "", "CPU limit")
cmd.Flags().String("limits-memory", "", "Memory limit")
cmd.Flags().Bool("health-check-enabled", false, "Enable health checks")
cmd.Flags().String("health-check-path", "", "Health check path")
return cmd
}
+153
View File
@@ -0,0 +1,153 @@
package create
import (
"fmt"
"github.com/spf13/cobra"
"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"
)
// NewGitHubCommand returns the create github application command
func NewGitHubCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "github",
Short: "Create an application from a private repository using GitHub App",
Long: `Create a new application from a private git repository using GitHub App authentication.
Use 'coolify github list' to find your GitHub App UUID.
Use 'coolify github repos <app-uuid>' to list accessible repositories.
Examples:
coolify app create github --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--github-app-uuid <uuid> --git-repository "owner/repo" --git-branch main \
--build-pack nixpacks --ports-exposes 3000
coolify app create github --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--github-app-uuid <uuid> --git-repository "owner/repo" --git-branch main \
--build-pack dockerfile --ports-exposes 8080 --instant-deploy`,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
// Get required flags
serverUUID, _ := cmd.Flags().GetString("server-uuid")
projectUUID, _ := cmd.Flags().GetString("project-uuid")
gitHubAppUUID, _ := cmd.Flags().GetString("github-app-uuid")
gitRepository, _ := cmd.Flags().GetString("git-repository")
gitBranch, _ := cmd.Flags().GetString("git-branch")
buildPack, _ := cmd.Flags().GetString("build-pack")
portsExposes, _ := cmd.Flags().GetString("ports-exposes")
environmentName, _ := cmd.Flags().GetString("environment-name")
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
// Validate required fields
if serverUUID == "" || projectUUID == "" {
return fmt.Errorf("--server-uuid and --project-uuid are required")
}
if gitHubAppUUID == "" {
return fmt.Errorf("--github-app-uuid is required")
}
if gitRepository == "" || gitBranch == "" {
return fmt.Errorf("--git-repository and --git-branch are required")
}
if buildPack == "" || portsExposes == "" {
return fmt.Errorf("--build-pack and --ports-exposes are required")
}
if environmentName == "" && environmentUUID == "" {
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
}
req := &models.ApplicationCreateGitHubAppRequest{
ServerUUID: serverUUID,
ProjectUUID: projectUUID,
GitHubAppUUID: gitHubAppUUID,
GitRepository: gitRepository,
GitBranch: gitBranch,
BuildPack: buildPack,
PortsExposes: portsExposes,
}
if environmentName != "" {
req.EnvironmentName = &environmentName
}
if environmentUUID != "" {
req.EnvironmentUUID = &environmentUUID
}
// Optional fields
setOptionalStringFlag(cmd, "name", &req.Name)
setOptionalStringFlag(cmd, "description", &req.Description)
setOptionalStringFlag(cmd, "domains", &req.Domains)
setOptionalStringFlag(cmd, "git-commit-sha", &req.GitCommitSHA)
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
setOptionalStringFlag(cmd, "build-command", &req.BuildCommand)
setOptionalStringFlag(cmd, "start-command", &req.StartCommand)
setOptionalStringFlag(cmd, "install-command", &req.InstallCommand)
setOptionalStringFlag(cmd, "base-directory", &req.BaseDirectory)
setOptionalStringFlag(cmd, "publish-directory", &req.PublishDirectory)
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
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.CreateGitHubApp(ctx, req)
if err != nil {
return fmt.Errorf("failed to create 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)
},
}
// Required 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("github-app-uuid", "", "GitHub App UUID (required)")
cmd.Flags().String("git-repository", "", "Git repository in format 'owner/repo' (required)")
cmd.Flags().String("git-branch", "", "Git branch (required)")
cmd.Flags().String("build-pack", "", "Build pack: nixpacks, static, dockerfile, dockercompose (required)")
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '3000' or '3000,8080' (required)")
// Optional flags
cmd.Flags().String("name", "", "Application name")
cmd.Flags().String("description", "", "Application description")
cmd.Flags().String("domains", "", "Domain(s) for the application")
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
cmd.Flags().String("git-commit-sha", "", "Specific commit SHA to deploy")
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
cmd.Flags().String("build-command", "", "Custom build command")
cmd.Flags().String("start-command", "", "Custom start command")
cmd.Flags().String("install-command", "", "Custom install command")
cmd.Flags().String("base-directory", "", "Base directory for the application")
cmd.Flags().String("publish-directory", "", "Publish directory for static builds")
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
cmd.Flags().String("limits-cpus", "", "CPU limit")
cmd.Flags().String("limits-memory", "", "Memory limit")
cmd.Flags().Bool("health-check-enabled", false, "Enable health checks")
cmd.Flags().String("health-check-path", "", "Health check path")
return cmd
}
+158
View File
@@ -0,0 +1,158 @@
package create
import (
"fmt"
"github.com/spf13/cobra"
"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"
)
// NewPublicCommand returns the create public application command
func NewPublicCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "public",
Short: "Create an application from a public git repository",
Long: `Create a new application from a public git repository.
Examples:
coolify app create public --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--git-repository "https://github.com/user/repo" --git-branch main --build-pack nixpacks --ports-exposes 3000
coolify app create public --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
--git-repository "https://github.com/user/repo" --git-branch main --build-pack dockerfile --ports-exposes 8080 \
--instant-deploy --domains "myapp.example.com"`,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
// Get required flags
serverUUID, _ := cmd.Flags().GetString("server-uuid")
projectUUID, _ := cmd.Flags().GetString("project-uuid")
gitRepository, _ := cmd.Flags().GetString("git-repository")
gitBranch, _ := cmd.Flags().GetString("git-branch")
buildPack, _ := cmd.Flags().GetString("build-pack")
portsExposes, _ := cmd.Flags().GetString("ports-exposes")
environmentName, _ := cmd.Flags().GetString("environment-name")
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
// Validate required fields
if serverUUID == "" || projectUUID == "" {
return fmt.Errorf("--server-uuid and --project-uuid are required")
}
if gitRepository == "" || gitBranch == "" {
return fmt.Errorf("--git-repository and --git-branch are required")
}
if buildPack == "" || portsExposes == "" {
return fmt.Errorf("--build-pack and --ports-exposes are required")
}
if environmentName == "" && environmentUUID == "" {
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
}
req := &models.ApplicationCreatePublicRequest{
ServerUUID: serverUUID,
ProjectUUID: projectUUID,
GitRepository: gitRepository,
GitBranch: gitBranch,
BuildPack: buildPack,
PortsExposes: portsExposes,
}
if environmentName != "" {
req.EnvironmentName = &environmentName
}
if environmentUUID != "" {
req.EnvironmentUUID = &environmentUUID
}
// Optional fields
setOptionalStringFlag(cmd, "name", &req.Name)
setOptionalStringFlag(cmd, "description", &req.Description)
setOptionalStringFlag(cmd, "domains", &req.Domains)
setOptionalStringFlag(cmd, "git-commit-sha", &req.GitCommitSHA)
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
setOptionalStringFlag(cmd, "build-command", &req.BuildCommand)
setOptionalStringFlag(cmd, "start-command", &req.StartCommand)
setOptionalStringFlag(cmd, "install-command", &req.InstallCommand)
setOptionalStringFlag(cmd, "base-directory", &req.BaseDirectory)
setOptionalStringFlag(cmd, "publish-directory", &req.PublishDirectory)
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
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.CreatePublic(ctx, req)
if err != nil {
return fmt.Errorf("failed to create 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)
},
}
// Required 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("git-repository", "", "Git repository URL (required)")
cmd.Flags().String("git-branch", "", "Git branch (required)")
cmd.Flags().String("build-pack", "", "Build pack: nixpacks, static, dockerfile, dockercompose (required)")
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '3000' or '3000,8080' (required)")
// Optional flags
cmd.Flags().String("name", "", "Application name")
cmd.Flags().String("description", "", "Application description")
cmd.Flags().String("domains", "", "Domain(s) for the application")
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
cmd.Flags().String("git-commit-sha", "", "Specific commit SHA to deploy")
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
cmd.Flags().String("build-command", "", "Custom build command")
cmd.Flags().String("start-command", "", "Custom start command")
cmd.Flags().String("install-command", "", "Custom install command")
cmd.Flags().String("base-directory", "", "Base directory for the application")
cmd.Flags().String("publish-directory", "", "Publish directory for static builds")
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
cmd.Flags().String("limits-cpus", "", "CPU limit")
cmd.Flags().String("limits-memory", "", "Memory limit")
cmd.Flags().Bool("health-check-enabled", false, "Enable health checks")
cmd.Flags().String("health-check-path", "", "Health check path")
return cmd
}
// Helper functions for optional flags
func setOptionalStringFlag(cmd *cobra.Command, flagName string, target **string) {
if cmd.Flags().Changed(flagName) {
val, _ := cmd.Flags().GetString(flagName)
*target = &val
}
}
func setOptionalBoolFlag(cmd *cobra.Command, flagName string, target **bool) {
if cmd.Flags().Changed(flagName) {
val, _ := cmd.Flags().GetBool(flagName)
*target = &val
}
}
+6 -1
View File
@@ -31,6 +31,7 @@ func NewCreateEnvCommand() *cobra.Command {
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
isRuntime, _ := cmd.Flags().GetBool("runtime")
if key == "" {
return fmt.Errorf("--key is required")
@@ -56,6 +57,9 @@ func NewCreateEnvCommand() *cobra.Command {
if cmd.Flags().Changed("is-multiline") {
req.IsMultiline = &isMultiline
}
if cmd.Flags().Changed("runtime") {
req.IsRuntime = &isRuntime
}
appSvc := service.NewApplicationService(client)
env, err := appSvc.CreateEnv(ctx, appUUID, req)
@@ -71,9 +75,10 @@ func NewCreateEnvCommand() *cobra.Command {
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("build-time", true, "Available at build time (default: true)")
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")
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
return cmd
}
+5 -1
View File
@@ -11,7 +11,7 @@ import (
)
func NewGetEnvCommand() *cobra.Command {
return &cobra.Command{
cmd := &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.`,
@@ -27,6 +27,8 @@ func NewGetEnvCommand() *cobra.Command {
}
appSvc := service.NewApplicationService(client)
// First try to get by the identifier directly
env, err := appSvc.GetEnv(ctx, appUUID, envUUIDOrKey)
if err != nil {
return fmt.Errorf("failed to get environment variable: %w", err)
@@ -53,4 +55,6 @@ func NewGetEnvCommand() *cobra.Command {
return formatter.Format(env)
},
}
return cmd
}
+32 -2
View File
@@ -2,19 +2,21 @@ package env
import (
"fmt"
"sort"
"github.com/spf13/cobra"
"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"
)
func NewListEnvCommand() *cobra.Command {
return &cobra.Command{
cmd := &cobra.Command{
Use: "list <app_uuid>",
Short: "List all environment variables for an application",
Long: `List all environment variables for a specific application.`,
Long: `List all environment variables for a specific application. By default, only non-preview environment variables are shown. Use --preview to show preview environment variables instead, or --all to show all variables (non-preview first, then preview).`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
@@ -31,6 +33,29 @@ func NewListEnvCommand() *cobra.Command {
return fmt.Errorf("failed to list environment variables: %w", err)
}
// Filter by preview/all flags
showAll, _ := cmd.Flags().GetBool("all")
showPreview, _ := cmd.Flags().GetBool("preview")
if showAll {
// Sort: non-preview first, then preview
sort.SliceStable(envs, func(i, j int) bool {
if envs[i].IsPreview != envs[j].IsPreview {
return !envs[i].IsPreview // non-preview (false) comes before preview (true)
}
return false // maintain original order within groups
})
} else {
// Filter by preview flag
var filtered []models.EnvironmentVariable
for _, env := range envs {
if env.IsPreview == showPreview {
filtered = append(filtered, env)
}
}
envs = filtered
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
@@ -54,4 +79,9 @@ func NewListEnvCommand() *cobra.Command {
return formatter.Format(envs)
},
}
cmd.Flags().Bool("preview", false, "Show preview environment variables instead of regular ones")
cmd.Flags().Bool("all", false, "Show all environment variables (non-preview first, then preview)")
return cmd
}
+6 -1
View File
@@ -40,6 +40,7 @@ Example: coolify app env sync abc123 --file .env.production`,
isBuildTime, _ := cmd.Flags().GetBool("build-time")
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
isRuntime, _ := cmd.Flags().GetBool("runtime")
// Parse the .env file
envVars, err := parser.ParseEnvFile(filePath)
@@ -87,6 +88,9 @@ Example: coolify app env sync abc123 --file .env.production`,
if cmd.Flags().Changed("is-literal") {
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("runtime") {
req.IsRuntime = &isRuntime
}
// Auto-detect multiline values
if strings.Contains(envVar.Value, "\n") {
@@ -147,8 +151,9 @@ Example: coolify app env sync abc123 --file .env.production`,
}
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("build-time", true, "Make all variables available at build time (default: true)")
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)")
syncEnvCmd.Flags().Bool("runtime", true, "Make all variables available at runtime (default: true)")
return syncEnvCmd
}
+19 -9
View File
@@ -12,24 +12,26 @@ import (
func NewUpdateEnvCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <app_uuid> <env_uuid>",
Use: "update <app_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>"),
Long: `Update an existing environment variable. UUID is the application.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
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,
// Check minimum version requirement
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.469"); err != nil {
return err
}
req := &models.EnvironmentVariableUpdateRequest{}
if cmd.Flags().Changed("key") {
key, _ := cmd.Flags().GetString("key")
req.Key = &key
@@ -54,9 +56,16 @@ func NewUpdateEnvCommand() *cobra.Command {
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
req.IsMultiline = &isMultiline
}
if cmd.Flags().Changed("runtime") {
isRuntime, _ := cmd.Flags().GetBool("runtime")
req.IsRuntime = &isRuntime
}
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")
if req.Key == nil {
return fmt.Errorf("--key is required")
}
if req.Value == nil {
return fmt.Errorf("--value is required")
}
appSvc := service.NewApplicationService(client)
@@ -72,9 +81,10 @@ func NewUpdateEnvCommand() *cobra.Command {
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("build-time", true, "Available at build time (default: true)")
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")
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
return cmd
}
+70
View File
@@ -0,0 +1,70 @@
package project
import (
"fmt"
"github.com/spf13/cobra"
"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"
)
// NewCreateCommand returns the create project command
func NewCreateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Create a new project",
Long: `Create a new project in Coolify.
Examples:
coolify project create --name "My Project"
coolify project create --name "My Project" --description "A description"`,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
name, _ := cmd.Flags().GetString("name")
if name == "" {
return fmt.Errorf("--name is required")
}
req := &models.ProjectCreateRequest{
Name: name,
}
if cmd.Flags().Changed("description") {
desc, _ := cmd.Flags().GetString("description")
req.Description = &desc
}
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.Create(ctx, req)
if err != nil {
return fmt.Errorf("failed to create project: %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(project)
},
}
cmd.Flags().String("name", "", "Project name (required)")
cmd.Flags().String("description", "", "Project description")
return cmd
}
+1
View File
@@ -14,6 +14,7 @@ func NewProjectCommand() *cobra.Command {
// Add all project subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewCreateCommand())
return cmd
}
+2 -21
View File
@@ -6,7 +6,6 @@ import (
"log"
"os"
compareVersion "github.com/hashicorp/go-version"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -144,24 +143,6 @@ func initConfig() {
// They are loaded on-demand by getAPIClient() based on --instance or default instance
// This allows --instance flag to work correctly
// Check for updates
latestVersionStr, err := version.CheckLatestVersionOfCli(Debug)
if err != nil {
if Debug {
log.Println("Failed to check for updates:", err)
}
}
// Compare versions properly using semantic versioning
if latestVersionStr != "" {
latestVersion, err := compareVersion.NewVersion(latestVersionStr)
if err == nil {
currentVersion, err := compareVersion.NewVersion(version.GetVersion())
if err == nil && latestVersion.GreaterThan(currentVersion) {
if Debug {
log.Printf("New version of Coolify CLI is available: %s\n", latestVersionStr)
}
}
}
}
// Check for updates (errors are handled silently inside the function)
_, _ = version.CheckLatestVersionOfCli(Debug)
}
+252
View File
@@ -0,0 +1,252 @@
package service
import (
"fmt"
"github.com/spf13/cobra"
"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"
)
// validServiceTypes contains all supported one-click service types
var validServiceTypes = []string{
"activepieces",
"appsmith",
"appwrite",
"authentik",
"babybuddy",
"budge",
"changedetection",
"chatwoot",
"classicpress-with-mariadb",
"classicpress-with-mysql",
"classicpress-without-database",
"cloudflared",
"code-server",
"dashboard",
"directus",
"directus-with-postgresql",
"docker-registry",
"docuseal",
"docuseal-with-postgres",
"dokuwiki",
"duplicati",
"emby",
"embystat",
"fider",
"filebrowser",
"firefly",
"formbricks",
"ghost",
"gitea",
"gitea-with-mariadb",
"gitea-with-mysql",
"gitea-with-postgresql",
"glance",
"glances",
"glitchtip",
"grafana",
"grafana-with-postgresql",
"grocy",
"heimdall",
"homepage",
"jellyfin",
"kuzzle",
"listmonk",
"logto",
"mediawiki",
"meilisearch",
"metabase",
"metube",
"minio",
"moodle",
"n8n",
"n8n-with-postgresql",
"next-image-transformation",
"nextcloud",
"nocodb",
"odoo",
"openblocks",
"pairdrop",
"penpot",
"phpmyadmin",
"pocketbase",
"posthog",
"reactive-resume",
"rocketchat",
"shlink",
"slash",
"snapdrop",
"statusnook",
"stirling-pdf",
"supabase",
"syncthing",
"tolgee",
"trigger",
"trigger-with-external-database",
"twenty",
"umami",
"unleash-with-postgresql",
"unleash-without-database",
"uptime-kuma",
"vaultwarden",
"vikunja",
"weblate",
"whoogle",
"wordpress-with-mariadb",
"wordpress-with-mysql",
"wordpress-without-database",
}
func NewCreateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create <type>",
Short: "Create a new one-click service",
Long: `Create a new one-click service of the specified type.
Use 'coolify service create --list-types' to see all available service types.
Examples:
coolify service create wordpress-with-mysql --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production
coolify service create ghost --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production --name="My Blog"
coolify service create n8n --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production --instant-deploy
Popular service types:
- wordpress-with-mysql, wordpress-with-mariadb, wordpress-without-database
- ghost, plausible, umami, uptime-kuma
- n8n, n8n-with-postgresql
- nextcloud, gitea, minio
- grafana, metabase, nocodb
- supabase, pocketbase, appwrite`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Handle --list-types flag
listTypes, _ := cmd.Flags().GetBool("list-types")
if listTypes {
fmt.Println("Available one-click service types:")
fmt.Println()
for _, t := range validServiceTypes {
fmt.Printf(" %s\n", t)
}
return nil
}
// Require type argument if not listing
if len(args) == 0 {
return fmt.Errorf("service type is required. Use --list-types to see available types")
}
serviceType := args[0]
// Validate service type
isValid := false
for _, t := range validServiceTypes {
if t == serviceType {
isValid = true
break
}
}
if !isValid {
return fmt.Errorf("invalid service type '%s'. Use --list-types to see available types", serviceType)
}
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.ServiceCreateRequest{
Type: serviceType,
ServerUUID: serverUUID,
ProjectUUID: projectUUID,
}
if environmentName != "" {
req.EnvironmentName = environmentName
}
if environmentUUID != "" {
req.EnvironmentUUID = &environmentUUID
}
// Handle optional 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("destination-uuid") {
dest, _ := cmd.Flags().GetString("destination-uuid")
req.Destination = &dest
}
if cmd.Flags().Changed("instant-deploy") {
instant, _ := cmd.Flags().GetBool("instant-deploy")
req.InstantDeploy = &instant
}
if cmd.Flags().Changed("docker-compose") {
compose, _ := cmd.Flags().GetString("docker-compose")
req.DockerCompose = &compose
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
svc := service.NewService(client)
result, err := svc.Create(ctx, req)
if err != nil {
return fmt.Errorf("failed to create service: %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)
},
}
// List types flag
cmd.Flags().Bool("list-types", false, "List all available service types")
// Required 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")
// Optional flags
cmd.Flags().String("name", "", "Service name")
cmd.Flags().String("description", "", "Service description")
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
cmd.Flags().String("docker-compose", "", "Custom Docker Compose content (for advanced customization)")
// Add completion for service type positional argument
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return validServiceTypes, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
return cmd
}
+7 -7
View File
@@ -28,9 +28,9 @@ func NewCreateCommand() *cobra.Command {
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")
isRuntime, _ := cmd.Flags().GetBool("runtime")
if key == "" {
return fmt.Errorf("--key is required")
@@ -39,7 +39,7 @@ func NewCreateCommand() *cobra.Command {
return fmt.Errorf("--value is required")
}
req := &models.EnvironmentVariableCreateRequest{
req := &models.ServiceEnvironmentVariableCreateRequest{
Key: key,
Value: value,
}
@@ -48,15 +48,15 @@ func NewCreateCommand() *cobra.Command {
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
}
if cmd.Flags().Changed("runtime") {
req.IsRuntime = &isRuntime
}
serviceSvc := service.NewService(client)
env, err := serviceSvc.CreateEnv(ctx, uuid, req)
@@ -71,10 +71,10 @@ func NewCreateCommand() *cobra.Command {
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("build-time", true, "Available at build time (default: true)")
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
return cmd
}
+11 -11
View File
@@ -38,8 +38,8 @@ Example: coolify service env sync abc123 --file .env.production`,
}
isBuildTime, _ := cmd.Flags().GetBool("build-time")
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
isRuntime, _ := cmd.Flags().GetBool("runtime")
// Parse the .env file
envVars, err := parser.ParseEnvFile(filePath)
@@ -62,17 +62,17 @@ Example: coolify service env sync abc123 --file .env.production`,
}
// Build a map of existing env vars by key
existingMap := make(map[string]models.EnvironmentVariable)
existingMap := make(map[string]models.ServiceEnvironmentVariable)
for _, env := range existingEnvs {
existingMap[env.Key] = env
}
// Separate into updates and creates
var toUpdate []models.EnvironmentVariableCreateRequest
var toCreate []models.EnvironmentVariableCreateRequest
var toUpdate []models.ServiceEnvironmentVariableCreateRequest
var toCreate []models.ServiceEnvironmentVariableCreateRequest
for _, envVar := range envVars {
req := models.EnvironmentVariableCreateRequest{
req := models.ServiceEnvironmentVariableCreateRequest{
Key: envVar.Key,
Value: envVar.Value,
}
@@ -81,12 +81,12 @@ Example: coolify service env sync abc123 --file .env.production`,
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("runtime") {
req.IsRuntime = &isRuntime
}
// Auto-detect multiline values
if strings.Contains(envVar.Value, "\n") {
@@ -108,7 +108,7 @@ Example: coolify service env sync abc123 --file .env.production`,
// 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{
bulkReq := &models.ServiceEnvBulkUpdateRequest{
Data: toUpdate,
}
_, err := serviceSvc.BulkUpdateEnvs(ctx, uuid, bulkReq)
@@ -147,9 +147,9 @@ Example: coolify service env sync abc123 --file .env.production`,
}
cmd.Flags().StringP("file", "f", "", "Path to .env file (required)")
cmd.Flags().Bool("build-time", false, "Make all variables available at build time")
cmd.Flags().Bool("preview", false, "Make all variables available in preview deployments")
cmd.Flags().Bool("build-time", true, "Make all variables available at build time (default: true)")
cmd.Flags().Bool("is-literal", false, "Treat all values as literal (don't interpolate variables)")
cmd.Flags().Bool("runtime", true, "Make all variables available at runtime (default: true)")
return cmd
}
+19 -15
View File
@@ -12,24 +12,26 @@ import (
func NewUpdateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <service_uuid> <env_uuid>",
Use: "update <service_uuid>",
Short: "Update an environment variable",
Long: `Update an existing environment variable. First UUID is the service, second is the specific environment variable to update.`,
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
Long: `Update an existing environment variable. UUID is the service.`,
Args: cli.ExactArgs(1, "<service_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
serviceUUID := 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,
// Check minimum version requirement
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.469"); err != nil {
return err
}
req := &models.ServiceEnvironmentVariableUpdateRequest{}
// Only set fields that were provided
if cmd.Flags().Changed("key") {
key, _ := cmd.Flags().GetString("key")
@@ -43,10 +45,6 @@ func NewUpdateCommand() *cobra.Command {
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
@@ -55,10 +53,16 @@ func NewUpdateCommand() *cobra.Command {
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
req.IsMultiline = &isMultiline
}
if cmd.Flags().Changed("runtime") {
isRuntime, _ := cmd.Flags().GetBool("runtime")
req.IsRuntime = &isRuntime
}
// 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)")
if req.Key == nil {
return fmt.Errorf("--key is required")
}
if req.Value == nil {
return fmt.Errorf("--value is required")
}
serviceSvc := service.NewService(client)
@@ -74,10 +78,10 @@ func NewUpdateCommand() *cobra.Command {
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("build-time", true, "Available at build time (default: true)")
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
return cmd
}
+18 -10
View File
@@ -1,6 +1,10 @@
package service
import "github.com/spf13/cobra"
import (
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/cmd/service/env"
)
// NewServiceCommand creates the service parent command with all subcommands
func NewServiceCommand() *cobra.Command {
@@ -14,20 +18,24 @@ func NewServiceCommand() *cobra.Command {
// Add main service commands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewCreateCommand())
cmd.AddCommand(NewStartCommand())
cmd.AddCommand(NewStopCommand())
cmd.AddCommand(NewRestartCommand())
cmd.AddCommand(NewDeleteCommand())
// Add env subcommand (placeholder for now)
// TODO: Implement env commands
// envCmd := &cobra.Command{
// Use: "env",
// Short: "Manage service environment variables",
// }
// envCmd.AddCommand(env.NewListCommand())
// ... more env commands
// cmd.AddCommand(envCmd)
// Add env subcommand
envCmd := &cobra.Command{
Use: "env",
Short: "Manage service environment variables",
}
envCmd.AddCommand(env.NewListCommand())
envCmd.AddCommand(env.NewGetCommand())
envCmd.AddCommand(env.NewCreateCommand())
envCmd.AddCommand(env.NewUpdateCommand())
envCmd.AddCommand(env.NewDeleteCommand())
envCmd.AddCommand(env.NewSyncCommand())
cmd.AddCommand(envCmd)
return cmd
}
+3 -3
View File
@@ -37,10 +37,10 @@ require (
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/xanzy/go-gitlab v0.115.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+8 -8
View File
@@ -86,8 +86,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -97,15 +97,15 @@ golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwE
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+186 -2
View File
@@ -100,10 +100,12 @@ type EnvironmentVariable struct {
UUID string `json:"uuid"`
Key string `json:"key"`
Value string `json:"value" sensitive:"true"`
IsBuildTime bool `json:"is_build_time"`
IsBuildTime bool `json:"is_buildtime"`
IsPreview bool `json:"is_preview"`
IsLiteralValue bool `json:"is_literal"`
IsShownOnce bool `json:"is_shown_once"`
IsRuntime bool `json:"is_runtime"`
IsShared bool `json:"is_shared"`
RealValue *string `json:"real_value,omitempty" sensitive:"true"`
ApplicationID *int `json:"-" table:"-"`
CreatedAt string `json:"-" table:"-"`
@@ -118,15 +120,197 @@ type EnvironmentVariableCreateRequest struct {
IsPreview *bool `json:"is_preview,omitempty"`
IsLiteral *bool `json:"is_literal,omitempty"`
IsMultiline *bool `json:"is_multiline,omitempty"`
IsRuntime *bool `json:"is_runtime,omitempty"`
}
// EnvironmentVariableUpdateRequest represents the request to update an environment variable
type EnvironmentVariableUpdateRequest struct {
UUID string `json:"uuid"`
Key *string `json:"key,omitempty"`
Value *string `json:"value,omitempty"`
IsBuildTime *bool `json:"is_build_time,omitempty"`
IsPreview *bool `json:"is_preview,omitempty"`
IsLiteral *bool `json:"is_literal,omitempty"`
IsMultiline *bool `json:"is_multiline,omitempty"`
IsRuntime *bool `json:"is_runtime,omitempty"`
}
// ApplicationCreatePublicRequest for POST /applications/public
// Creates an application from a public git repository
type ApplicationCreatePublicRequest struct {
// Required fields
ProjectUUID string `json:"project_uuid"`
ServerUUID string `json:"server_uuid"`
GitRepository string `json:"git_repository"`
GitBranch string `json:"git_branch"`
BuildPack string `json:"build_pack"` // nixpacks, static, dockerfile, dockercompose
PortsExposes string `json:"ports_exposes"`
// Environment (one of these is required)
EnvironmentName *string `json:"environment_name,omitempty"`
EnvironmentUUID *string `json:"environment,omitempty"`
// Optional fields
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Domains *string `json:"domains,omitempty"`
InstantDeploy *bool `json:"instant_deploy,omitempty"`
GitCommitSHA *string `json:"git_commit_sha,omitempty"`
DestinationUUID *string `json:"destination_uuid,omitempty"`
BuildCommand *string `json:"build_command,omitempty"`
StartCommand *string `json:"start_command,omitempty"`
InstallCommand *string `json:"install_command,omitempty"`
BaseDirectory *string `json:"base_directory,omitempty"`
PublishDirectory *string `json:"publish_directory,omitempty"`
PortsMappings *string `json:"ports_mappings,omitempty"`
CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"`
CustomLabels *string `json:"custom_labels,omitempty"`
// Health checks
HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"`
HealthCheckPath *string `json:"health_check_path,omitempty"`
HealthCheckPort *string `json:"health_check_port,omitempty"`
HealthCheckMethod *string `json:"health_check_method,omitempty"`
// Resource limits
LimitsCPUs *string `json:"limits_cpus,omitempty"`
LimitsMemory *string `json:"limits_memory,omitempty"`
}
// ApplicationCreateGitHubAppRequest for POST /applications/private-github-app
// Creates an application from a private repository using GitHub App authentication
type ApplicationCreateGitHubAppRequest struct {
// Required fields
ProjectUUID string `json:"project_uuid"`
ServerUUID string `json:"server_uuid"`
GitHubAppUUID string `json:"github_app_uuid"`
GitRepository string `json:"git_repository"`
GitBranch string `json:"git_branch"`
BuildPack string `json:"build_pack"`
PortsExposes string `json:"ports_exposes"`
// Environment (one of these is required)
EnvironmentName *string `json:"environment_name,omitempty"`
EnvironmentUUID *string `json:"environment,omitempty"`
// Optional fields (same as public)
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Domains *string `json:"domains,omitempty"`
InstantDeploy *bool `json:"instant_deploy,omitempty"`
GitCommitSHA *string `json:"git_commit_sha,omitempty"`
DestinationUUID *string `json:"destination_uuid,omitempty"`
BuildCommand *string `json:"build_command,omitempty"`
StartCommand *string `json:"start_command,omitempty"`
InstallCommand *string `json:"install_command,omitempty"`
BaseDirectory *string `json:"base_directory,omitempty"`
PublishDirectory *string `json:"publish_directory,omitempty"`
PortsMappings *string `json:"ports_mappings,omitempty"`
CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"`
CustomLabels *string `json:"custom_labels,omitempty"`
HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"`
HealthCheckPath *string `json:"health_check_path,omitempty"`
HealthCheckPort *string `json:"health_check_port,omitempty"`
HealthCheckMethod *string `json:"health_check_method,omitempty"`
LimitsCPUs *string `json:"limits_cpus,omitempty"`
LimitsMemory *string `json:"limits_memory,omitempty"`
}
// ApplicationCreateDeployKeyRequest for POST /applications/private-deploy-key
// Creates an application from a private repository using SSH deploy key
type ApplicationCreateDeployKeyRequest struct {
// Required fields
ProjectUUID string `json:"project_uuid"`
ServerUUID string `json:"server_uuid"`
PrivateKeyUUID string `json:"private_key_uuid"`
GitRepository string `json:"git_repository"`
GitBranch string `json:"git_branch"`
BuildPack string `json:"build_pack"`
PortsExposes string `json:"ports_exposes"`
// Environment (one of these is required)
EnvironmentName *string `json:"environment_name,omitempty"`
EnvironmentUUID *string `json:"environment,omitempty"`
// Optional fields (same as public)
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Domains *string `json:"domains,omitempty"`
InstantDeploy *bool `json:"instant_deploy,omitempty"`
GitCommitSHA *string `json:"git_commit_sha,omitempty"`
DestinationUUID *string `json:"destination_uuid,omitempty"`
BuildCommand *string `json:"build_command,omitempty"`
StartCommand *string `json:"start_command,omitempty"`
InstallCommand *string `json:"install_command,omitempty"`
BaseDirectory *string `json:"base_directory,omitempty"`
PublishDirectory *string `json:"publish_directory,omitempty"`
PortsMappings *string `json:"ports_mappings,omitempty"`
CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"`
CustomLabels *string `json:"custom_labels,omitempty"`
HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"`
HealthCheckPath *string `json:"health_check_path,omitempty"`
HealthCheckPort *string `json:"health_check_port,omitempty"`
HealthCheckMethod *string `json:"health_check_method,omitempty"`
LimitsCPUs *string `json:"limits_cpus,omitempty"`
LimitsMemory *string `json:"limits_memory,omitempty"`
}
// ApplicationCreateDockerfileRequest for POST /applications/dockerfile
// Creates an application from a custom Dockerfile
type ApplicationCreateDockerfileRequest struct {
// Required fields
ProjectUUID string `json:"project_uuid"`
ServerUUID string `json:"server_uuid"`
Dockerfile string `json:"dockerfile"`
// Environment (one of these is required)
EnvironmentName *string `json:"environment_name,omitempty"`
EnvironmentUUID *string `json:"environment,omitempty"`
// Optional fields
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Domains *string `json:"domains,omitempty"`
InstantDeploy *bool `json:"instant_deploy,omitempty"`
DestinationUUID *string `json:"destination_uuid,omitempty"`
PortsExposes *string `json:"ports_exposes,omitempty"`
PortsMappings *string `json:"ports_mappings,omitempty"`
CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"`
CustomLabels *string `json:"custom_labels,omitempty"`
HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"`
HealthCheckPath *string `json:"health_check_path,omitempty"`
HealthCheckPort *string `json:"health_check_port,omitempty"`
HealthCheckMethod *string `json:"health_check_method,omitempty"`
LimitsCPUs *string `json:"limits_cpus,omitempty"`
LimitsMemory *string `json:"limits_memory,omitempty"`
}
// ApplicationCreateDockerImageRequest for POST /applications/dockerimage
// Creates an application from a pre-built Docker image
type ApplicationCreateDockerImageRequest struct {
// Required fields
ProjectUUID string `json:"project_uuid"`
ServerUUID string `json:"server_uuid"`
DockerRegistryImageName string `json:"docker_registry_image_name"`
PortsExposes string `json:"ports_exposes"`
// Environment (one of these is required)
EnvironmentName *string `json:"environment_name,omitempty"`
EnvironmentUUID *string `json:"environment,omitempty"`
// Optional fields
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Domains *string `json:"domains,omitempty"`
InstantDeploy *bool `json:"instant_deploy,omitempty"`
DestinationUUID *string `json:"destination_uuid,omitempty"`
DockerRegistryImageTag *string `json:"docker_registry_image_tag,omitempty"`
PortsMappings *string `json:"ports_mappings,omitempty"`
CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"`
CustomLabels *string `json:"custom_labels,omitempty"`
HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"`
HealthCheckPath *string `json:"health_check_path,omitempty"`
HealthCheckPort *string `json:"health_check_port,omitempty"`
HealthCheckMethod *string `json:"health_check_method,omitempty"`
LimitsCPUs *string `json:"limits_cpus,omitempty"`
LimitsMemory *string `json:"limits_memory,omitempty"`
}
+130
View File
@@ -227,3 +227,133 @@ func TestServerCreateRequest_Marshal(t *testing.T) {
assert.Equal(t, request.Port, unmarshaled.Port)
assert.True(t, unmarshaled.InstantValidate)
}
func TestEnvironmentVariable_IsBuildtimeField(t *testing.T) {
// Test that is_buildtime (without underscore) unmarshals correctly
jsonData := `{
"uuid": "env-123",
"key": "TEST_VAR",
"value": "test_value",
"is_buildtime": true,
"is_preview": false,
"is_literal": false,
"is_shown_once": false,
"is_runtime": true,
"is_shared": false
}`
var env EnvironmentVariable
err := json.Unmarshal([]byte(jsonData), &env)
require.NoError(t, err)
assert.Equal(t, "env-123", env.UUID)
assert.Equal(t, "TEST_VAR", env.Key)
assert.True(t, env.IsBuildTime, "is_buildtime should unmarshal to true")
assert.True(t, env.IsRuntime, "is_runtime should unmarshal to true")
assert.False(t, env.IsShared, "is_shared should unmarshal to false")
}
func TestEnvironmentVariable_MarshalUnmarshal(t *testing.T) {
realValue := "secret_value"
env := EnvironmentVariable{
UUID: "env-uuid-123",
Key: "DATABASE_URL",
Value: "postgres://localhost/db",
IsBuildTime: true,
IsPreview: false,
IsLiteralValue: true,
IsShownOnce: false,
IsRuntime: true,
IsShared: false,
RealValue: &realValue,
}
// Marshal
data, err := json.Marshal(env)
require.NoError(t, err)
// Verify JSON contains is_buildtime (not is_build_time)
assert.Contains(t, string(data), `"is_buildtime":true`)
assert.NotContains(t, string(data), `"is_build_time"`)
// Unmarshal
var unmarshaled EnvironmentVariable
err = json.Unmarshal(data, &unmarshaled)
require.NoError(t, err)
assert.Equal(t, env.UUID, unmarshaled.UUID)
assert.Equal(t, env.Key, unmarshaled.Key)
assert.Equal(t, env.Value, unmarshaled.Value)
assert.True(t, unmarshaled.IsBuildTime)
assert.True(t, unmarshaled.IsLiteralValue)
assert.True(t, unmarshaled.IsRuntime)
assert.False(t, unmarshaled.IsShared)
assert.NotNil(t, unmarshaled.RealValue)
assert.Equal(t, *env.RealValue, *unmarshaled.RealValue)
}
func TestEnvironmentVariable_UnmarshalFromFixture(t *testing.T) {
fixtureData, err := os.ReadFile(filepath.Join("..", "..", "test", "fixtures", "environment_variable_complete.json"))
require.NoError(t, err)
var env EnvironmentVariable
err = json.Unmarshal(fixtureData, &env)
require.NoError(t, err)
assert.Equal(t, "env-test-uuid-123", env.UUID)
assert.Equal(t, "DATABASE_URL", env.Key)
assert.Equal(t, "postgres://localhost/mydb", env.Value)
assert.True(t, env.IsBuildTime, "IsBuildTime should be true from fixture")
assert.True(t, env.IsRuntime, "IsRuntime should be true from fixture")
assert.False(t, env.IsShared, "IsShared should be false from fixture")
assert.False(t, env.IsPreview)
assert.False(t, env.IsLiteralValue)
assert.False(t, env.IsShownOnce)
assert.NotNil(t, env.RealValue)
assert.Equal(t, "postgres://user:pass@localhost/mydb", *env.RealValue)
}
func TestEnvironmentVariable_PartialResponse(t *testing.T) {
// Test backward compatibility with older API responses that might not have all fields
jsonData := `{
"uuid": "env-123",
"key": "OLD_VAR",
"value": "old_value"
}`
var env EnvironmentVariable
err := json.Unmarshal([]byte(jsonData), &env)
require.NoError(t, err)
assert.Equal(t, "env-123", env.UUID)
assert.Equal(t, "OLD_VAR", env.Key)
assert.False(t, env.IsBuildTime, "Missing boolean fields should default to false")
assert.False(t, env.IsRuntime, "Missing boolean fields should default to false")
assert.False(t, env.IsShared, "Missing boolean fields should default to false")
}
func TestEnvironmentVariableCreateRequest_Marshal(t *testing.T) {
isBuildTime := true
isPreview := false
request := EnvironmentVariableCreateRequest{
Key: "NEW_VAR",
Value: "new_value",
IsBuildTime: &isBuildTime,
IsPreview: &isPreview,
}
data, err := json.Marshal(request)
require.NoError(t, err)
// Request models should still use is_build_time (with underscore) per API spec
assert.Contains(t, string(data), `"is_build_time":true`)
var unmarshaled EnvironmentVariableCreateRequest
err = json.Unmarshal(data, &unmarshaled)
require.NoError(t, err)
assert.Equal(t, request.Key, unmarshaled.Key)
assert.Equal(t, request.Value, unmarshaled.Value)
assert.NotNil(t, unmarshaled.IsBuildTime)
assert.True(t, *unmarshaled.IsBuildTime)
}
+48 -1
View File
@@ -51,7 +51,8 @@ type ServiceCreateRequest struct {
Description *string `json:"description,omitempty"`
ServerUUID string `json:"server_uuid"`
ProjectUUID string `json:"project_uuid"`
EnvironmentName string `json:"environment_name"`
EnvironmentName string `json:"environment_name,omitempty"`
EnvironmentUUID *string `json:"environment,omitempty"`
InstantDeploy *bool `json:"instant_deploy,omitempty"`
DockerCompose *string `json:"docker_compose,omitempty"`
Destination *string `json:"destination,omitempty"`
@@ -68,3 +69,49 @@ type ServiceUpdateRequest struct {
type ServiceLifecycleResponse struct {
Message string `json:"message"`
}
// ServiceEnvironmentVariable represents an environment variable for a service
// Services don't have preview deployments, so IsPreview is excluded from output
type ServiceEnvironmentVariable struct {
ID int `json:"-" table:"-"`
UUID string `json:"uuid"`
Key string `json:"key"`
Value string `json:"value" sensitive:"true"`
IsBuildTime bool `json:"is_buildtime"`
IsLiteralValue bool `json:"is_literal"`
IsShownOnce bool `json:"is_shown_once"`
IsRuntime bool `json:"is_runtime"`
IsShared bool `json:"is_shared"`
RealValue *string `json:"real_value,omitempty" sensitive:"true"`
ServiceID *int `json:"-" table:"-"`
CreatedAt string `json:"-" table:"-"`
UpdatedAt string `json:"-" table:"-"`
}
// ServiceEnvironmentVariableCreateRequest represents the request to create a service environment variable
type ServiceEnvironmentVariableCreateRequest struct {
Key string `json:"key"`
Value string `json:"value"`
IsBuildTime *bool `json:"is_build_time,omitempty"`
IsLiteral *bool `json:"is_literal,omitempty"`
IsMultiline *bool `json:"is_multiline,omitempty"`
IsRuntime *bool `json:"is_runtime,omitempty"`
}
// ServiceEnvironmentVariableUpdateRequest represents the request to update a service environment variable
type ServiceEnvironmentVariableUpdateRequest struct {
Key *string `json:"key,omitempty"`
Value *string `json:"value,omitempty"`
IsBuildTime *bool `json:"is_build_time,omitempty"`
IsLiteral *bool `json:"is_literal,omitempty"`
IsMultiline *bool `json:"is_multiline,omitempty"`
IsRuntime *bool `json:"is_runtime,omitempty"`
}
// ServiceEnvBulkUpdateRequest represents the request to bulk update service environment variables
type ServiceEnvBulkUpdateRequest struct {
Data []ServiceEnvironmentVariableCreateRequest `json:"data"`
}
// ServiceEnvBulkUpdateResponse represents the response from service bulk update
type ServiceEnvBulkUpdateResponse []ServiceEnvironmentVariable
+50
View File
@@ -197,3 +197,53 @@ func (s *ApplicationService) BulkUpdateEnvs(ctx context.Context, appUUID string,
}
return &response, nil
}
// CreatePublic creates an application from a public git repository
func (s *ApplicationService) CreatePublic(ctx context.Context, req *models.ApplicationCreatePublicRequest) (*models.Application, error) {
var app models.Application
err := s.client.Post(ctx, "applications/public", req, &app)
if err != nil {
return nil, fmt.Errorf("failed to create application from public repository: %w", err)
}
return &app, nil
}
// CreateGitHubApp creates an application from a private repository using GitHub App
func (s *ApplicationService) CreateGitHubApp(ctx context.Context, req *models.ApplicationCreateGitHubAppRequest) (*models.Application, error) {
var app models.Application
err := s.client.Post(ctx, "applications/private-github-app", req, &app)
if err != nil {
return nil, fmt.Errorf("failed to create application from private GitHub repository: %w", err)
}
return &app, nil
}
// CreateDeployKey creates an application from a private repository using SSH deploy key
func (s *ApplicationService) CreateDeployKey(ctx context.Context, req *models.ApplicationCreateDeployKeyRequest) (*models.Application, error) {
var app models.Application
err := s.client.Post(ctx, "applications/private-deploy-key", req, &app)
if err != nil {
return nil, fmt.Errorf("failed to create application from private repository with deploy key: %w", err)
}
return &app, nil
}
// CreateDockerfile creates an application from a custom Dockerfile
func (s *ApplicationService) CreateDockerfile(ctx context.Context, req *models.ApplicationCreateDockerfileRequest) (*models.Application, error) {
var app models.Application
err := s.client.Post(ctx, "applications/dockerfile", req, &app)
if err != nil {
return nil, fmt.Errorf("failed to create application from Dockerfile: %w", err)
}
return &app, nil
}
// CreateDockerImage creates an application from a pre-built Docker image
func (s *ApplicationService) CreateDockerImage(ctx context.Context, req *models.ApplicationCreateDockerImageRequest) (*models.Application, error) {
var app models.Application
err := s.client.Post(ctx, "applications/dockerimage", req, &app)
if err != nil {
return nil, fmt.Errorf("failed to create application from Docker image: %w", err)
}
return &app, nil
}
+397 -2
View File
@@ -737,9 +737,10 @@ func TestApplicationService_UpdateEnv(t *testing.T) {
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
newKey := "API_KEY"
newValue := "newsecret456"
req := &models.EnvironmentVariableUpdateRequest{
UUID: "env-uuid-1",
Key: &newKey,
Value: &newValue,
}
@@ -760,9 +761,10 @@ func TestApplicationService_UpdateEnv_Error(t *testing.T) {
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
newKey := "API_KEY"
newValue := "newsecret456"
req := &models.EnvironmentVariableUpdateRequest{
UUID: "env-uuid-1",
Key: &newKey,
Value: &newValue,
}
@@ -801,3 +803,396 @@ func TestApplicationService_DeleteEnv_Error(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to delete environment variable")
}
func TestApplicationService_ListEnvs_AllFields(t *testing.T) {
// Test that all fields including is_buildtime (without underscore), is_runtime, and is_shared
// are correctly parsed from API response
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/app-uuid-123/envs", r.URL.Path)
assert.Equal(t, "GET", r.Method)
// Mock API response with all fields
envs := []models.EnvironmentVariable{
{
UUID: "env-1",
Key: "DATABASE_URL",
Value: "postgres://localhost",
IsBuildTime: true,
IsPreview: false,
IsLiteralValue: true,
IsShownOnce: false,
IsRuntime: true,
IsShared: false,
},
{
UUID: "env-2",
Key: "API_KEY",
Value: "secret",
IsBuildTime: false,
IsPreview: true,
IsLiteralValue: false,
IsShownOnce: false,
IsRuntime: false,
IsShared: true,
},
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(envs)
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
result, err := svc.ListEnvs(context.Background(), "app-uuid-123")
require.NoError(t, err)
assert.Len(t, result, 2)
// Verify first env var
assert.Equal(t, "DATABASE_URL", result[0].Key)
assert.True(t, result[0].IsBuildTime, "IsBuildTime should be true for DATABASE_URL")
assert.True(t, result[0].IsRuntime, "IsRuntime should be true for DATABASE_URL")
assert.False(t, result[0].IsShared, "IsShared should be false for DATABASE_URL")
assert.True(t, result[0].IsLiteralValue)
assert.False(t, result[0].IsPreview)
// Verify second env var
assert.Equal(t, "API_KEY", result[1].Key)
assert.False(t, result[1].IsBuildTime, "IsBuildTime should be false for API_KEY")
assert.False(t, result[1].IsRuntime, "IsRuntime should be false for API_KEY")
assert.True(t, result[1].IsShared, "IsShared should be true for API_KEY")
assert.False(t, result[1].IsLiteralValue)
assert.True(t, result[1].IsPreview)
}
func TestApplicationService_EnvBuildtimeFlag(t *testing.T) {
// Test specifically that is_buildtime (without underscore) unmarshals correctly
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/app-uuid-123/envs", r.URL.Path)
// Directly write JSON with is_buildtime (no underscore) to mimic actual API
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[
{
"uuid": "env-test-1",
"key": "BUILD_VAR",
"value": "build_value",
"is_buildtime": true,
"is_preview": false,
"is_literal": false,
"is_shown_once": false,
"is_runtime": true,
"is_shared": false
}
]`))
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
result, err := svc.ListEnvs(context.Background(), "app-uuid-123")
require.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, "BUILD_VAR", result[0].Key)
assert.True(t, result[0].IsBuildTime, "is_buildtime field should unmarshal correctly to true")
assert.True(t, result[0].IsRuntime, "is_runtime field should unmarshal correctly to true")
}
func TestApplicationService_EnvRuntimeAndShared(t *testing.T) {
// Test is_runtime and is_shared fields specifically
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/app-uuid-123/envs", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[
{
"uuid": "env-runtime",
"key": "RUNTIME_VAR",
"value": "runtime_value",
"is_buildtime": false,
"is_preview": false,
"is_literal": false,
"is_shown_once": false,
"is_runtime": true,
"is_shared": false
},
{
"uuid": "env-shared",
"key": "SHARED_VAR",
"value": "shared_value",
"is_buildtime": false,
"is_preview": false,
"is_literal": false,
"is_shown_once": false,
"is_runtime": false,
"is_shared": true
}
]`))
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
result, err := svc.ListEnvs(context.Background(), "app-uuid-123")
require.NoError(t, err)
assert.Len(t, result, 2)
// Verify runtime var
assert.Equal(t, "RUNTIME_VAR", result[0].Key)
assert.True(t, result[0].IsRuntime, "IsRuntime should be true")
assert.False(t, result[0].IsShared, "IsShared should be false")
// Verify shared var
assert.Equal(t, "SHARED_VAR", result[1].Key)
assert.False(t, result[1].IsRuntime, "IsRuntime should be false")
assert.True(t, result[1].IsShared, "IsShared should be true")
}
func TestApplicationService_CreatePublic(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/public", r.URL.Path)
assert.Equal(t, "POST", r.Method)
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
var req models.ApplicationCreatePublicRequest
err := json.NewDecoder(r.Body).Decode(&req)
assert.NoError(t, err)
assert.Equal(t, "proj-uuid", req.ProjectUUID)
assert.Equal(t, "server-uuid", req.ServerUUID)
assert.Equal(t, "https://github.com/user/repo", req.GitRepository)
assert.Equal(t, "main", req.GitBranch)
assert.Equal(t, "nixpacks", req.BuildPack)
assert.Equal(t, "3000", req.PortsExposes)
branch := "main"
fqdn := "app.example.com"
app := models.Application{
UUID: "new-app-uuid",
Name: "My App",
Status: "starting",
GitBranch: &branch,
FQDN: &fqdn,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(app)
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
envName := "production"
req := &models.ApplicationCreatePublicRequest{
ProjectUUID: "proj-uuid",
ServerUUID: "server-uuid",
GitRepository: "https://github.com/user/repo",
GitBranch: "main",
BuildPack: "nixpacks",
PortsExposes: "3000",
EnvironmentName: &envName,
}
result, err := svc.CreatePublic(context.Background(), req)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "new-app-uuid", result.UUID)
assert.Equal(t, "My App", result.Name)
}
func TestApplicationService_CreatePublic_Error(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"message":"invalid repository URL"}`))
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
envName := "production"
req := &models.ApplicationCreatePublicRequest{
ProjectUUID: "proj-uuid",
ServerUUID: "server-uuid",
GitRepository: "invalid-repo",
GitBranch: "main",
BuildPack: "nixpacks",
PortsExposes: "3000",
EnvironmentName: &envName,
}
result, err := svc.CreatePublic(context.Background(), req)
require.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "failed to create application from public repository")
}
func TestApplicationService_CreateGitHubApp(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/private-github-app", r.URL.Path)
assert.Equal(t, "POST", r.Method)
var req models.ApplicationCreateGitHubAppRequest
err := json.NewDecoder(r.Body).Decode(&req)
assert.NoError(t, err)
assert.Equal(t, "github-app-uuid", req.GitHubAppUUID)
assert.Equal(t, "owner/repo", req.GitRepository)
branch := "main"
app := models.Application{
UUID: "new-app-uuid",
Name: "Private App",
Status: "starting",
GitBranch: &branch,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(app)
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
envName := "production"
req := &models.ApplicationCreateGitHubAppRequest{
ProjectUUID: "proj-uuid",
ServerUUID: "server-uuid",
GitHubAppUUID: "github-app-uuid",
GitRepository: "owner/repo",
GitBranch: "main",
BuildPack: "nixpacks",
PortsExposes: "3000",
EnvironmentName: &envName,
}
result, err := svc.CreateGitHubApp(context.Background(), req)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "new-app-uuid", result.UUID)
}
func TestApplicationService_CreateDeployKey(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/private-deploy-key", r.URL.Path)
assert.Equal(t, "POST", r.Method)
var req models.ApplicationCreateDeployKeyRequest
err := json.NewDecoder(r.Body).Decode(&req)
assert.NoError(t, err)
assert.Equal(t, "key-uuid", req.PrivateKeyUUID)
assert.Equal(t, "git@github.com:owner/repo.git", req.GitRepository)
branch := "main"
app := models.Application{
UUID: "new-app-uuid",
Name: "Deploy Key App",
Status: "starting",
GitBranch: &branch,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(app)
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
envName := "production"
req := &models.ApplicationCreateDeployKeyRequest{
ProjectUUID: "proj-uuid",
ServerUUID: "server-uuid",
PrivateKeyUUID: "key-uuid",
GitRepository: "git@github.com:owner/repo.git",
GitBranch: "main",
BuildPack: "nixpacks",
PortsExposes: "3000",
EnvironmentName: &envName,
}
result, err := svc.CreateDeployKey(context.Background(), req)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "new-app-uuid", result.UUID)
}
func TestApplicationService_CreateDockerfile(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/dockerfile", r.URL.Path)
assert.Equal(t, "POST", r.Method)
var req models.ApplicationCreateDockerfileRequest
err := json.NewDecoder(r.Body).Decode(&req)
assert.NoError(t, err)
assert.Contains(t, req.Dockerfile, "FROM node:18")
app := models.Application{
UUID: "new-app-uuid",
Name: "Dockerfile App",
Status: "starting",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(app)
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
envName := "production"
req := &models.ApplicationCreateDockerfileRequest{
ProjectUUID: "proj-uuid",
ServerUUID: "server-uuid",
Dockerfile: "FROM node:18\nCOPY . .\nCMD [\"node\", \"app.js\"]",
EnvironmentName: &envName,
}
result, err := svc.CreateDockerfile(context.Background(), req)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "new-app-uuid", result.UUID)
}
func TestApplicationService_CreateDockerImage(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/applications/dockerimage", r.URL.Path)
assert.Equal(t, "POST", r.Method)
var req models.ApplicationCreateDockerImageRequest
err := json.NewDecoder(r.Body).Decode(&req)
assert.NoError(t, err)
assert.Equal(t, "nginx:latest", req.DockerRegistryImageName)
assert.Equal(t, "80", req.PortsExposes)
app := models.Application{
UUID: "new-app-uuid",
Name: "Docker Image App",
Status: "starting",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(app)
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewApplicationService(client)
envName := "production"
req := &models.ApplicationCreateDockerImageRequest{
ProjectUUID: "proj-uuid",
ServerUUID: "server-uuid",
DockerRegistryImageName: "nginx:latest",
PortsExposes: "80",
EnvironmentName: &envName,
}
result, err := svc.CreateDockerImage(context.Background(), req)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "new-app-uuid", result.UUID)
}
+10
View File
@@ -39,3 +39,13 @@ func (s *ProjectService) Get(ctx context.Context, uuid string) (*models.Project,
}
return &project, nil
}
// Create creates a new project
func (s *ProjectService) Create(ctx context.Context, req *models.ProjectCreateRequest) (*models.Project, error) {
var project models.Project
err := s.client.Post(ctx, "projects", req, &project)
if err != nil {
return nil, fmt.Errorf("failed to create project: %w", err)
}
return &project, nil
}
+68
View File
@@ -74,3 +74,71 @@ func TestProjectService_Get(t *testing.T) {
assert.Equal(t, "proj-1", result.UUID)
assert.Equal(t, "Test Project", result.Name)
}
func TestProjectService_Create(t *testing.T) {
desc := "New Project Description"
project := models.Project{
UUID: "proj-new",
Name: "New Project",
Description: &desc,
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/projects", r.URL.Path)
assert.Equal(t, "POST", r.Method)
var req models.ProjectCreateRequest
err := json.NewDecoder(r.Body).Decode(&req)
assert.NoError(t, err)
assert.Equal(t, "New Project", req.Name)
assert.NotNil(t, req.Description)
assert.Equal(t, "New Project Description", *req.Description)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(project)
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewProjectService(client)
result, err := svc.Create(context.Background(), &models.ProjectCreateRequest{
Name: "New Project",
Description: &desc,
})
require.NoError(t, err)
assert.Equal(t, "proj-new", result.UUID)
assert.Equal(t, "New Project", result.Name)
}
func TestProjectService_Create_NameOnly(t *testing.T) {
project := models.Project{
UUID: "proj-minimal",
Name: "Minimal Project",
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/projects", r.URL.Path)
assert.Equal(t, "POST", r.Method)
var req models.ProjectCreateRequest
err := json.NewDecoder(r.Body).Decode(&req)
assert.NoError(t, err)
assert.Equal(t, "Minimal Project", req.Name)
assert.Nil(t, req.Description)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(project)
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewProjectService(client)
result, err := svc.Create(context.Background(), &models.ProjectCreateRequest{
Name: "Minimal Project",
})
require.NoError(t, err)
assert.Equal(t, "proj-minimal", result.UUID)
assert.Equal(t, "Minimal Project", result.Name)
}
+10 -10
View File
@@ -101,8 +101,8 @@ func (s *Service) Restart(ctx context.Context, uuid string) (*models.ServiceLife
}
// ListEnvs retrieves all environment variables for a service
func (s *Service) ListEnvs(ctx context.Context, uuid string) ([]models.EnvironmentVariable, error) {
var envs []models.EnvironmentVariable
func (s *Service) ListEnvs(ctx context.Context, uuid string) ([]models.ServiceEnvironmentVariable, error) {
var envs []models.ServiceEnvironmentVariable
err := s.client.Get(ctx, fmt.Sprintf("services/%s/envs", uuid), &envs)
if err != nil {
return nil, fmt.Errorf("failed to list environment variables for service %s: %w", uuid, err)
@@ -111,7 +111,7 @@ func (s *Service) ListEnvs(ctx context.Context, uuid string) ([]models.Environme
}
// GetEnv retrieves a single environment variable by UUID or key
func (s *Service) GetEnv(ctx context.Context, serviceUUID, envIdentifier string) (*models.EnvironmentVariable, error) {
func (s *Service) GetEnv(ctx context.Context, serviceUUID, envIdentifier string) (*models.ServiceEnvironmentVariable, error) {
envs, err := s.ListEnvs(ctx, serviceUUID)
if err != nil {
return nil, err
@@ -128,8 +128,8 @@ func (s *Service) GetEnv(ctx context.Context, serviceUUID, envIdentifier string)
}
// CreateEnv creates a new environment variable for a service
func (s *Service) CreateEnv(ctx context.Context, uuid string, req *models.EnvironmentVariableCreateRequest) (*models.EnvironmentVariable, error) {
var env models.EnvironmentVariable
func (s *Service) CreateEnv(ctx context.Context, uuid string, req *models.ServiceEnvironmentVariableCreateRequest) (*models.ServiceEnvironmentVariable, error) {
var env models.ServiceEnvironmentVariable
err := s.client.Post(ctx, fmt.Sprintf("services/%s/envs", uuid), req, &env)
if err != nil {
return nil, fmt.Errorf("failed to create environment variable for service %s: %w", uuid, err)
@@ -138,8 +138,8 @@ func (s *Service) CreateEnv(ctx context.Context, uuid string, req *models.Enviro
}
// UpdateEnv updates an environment variable for a service
func (s *Service) UpdateEnv(ctx context.Context, serviceUUID string, req *models.EnvironmentVariableUpdateRequest) (*models.EnvironmentVariable, error) {
var env models.EnvironmentVariable
func (s *Service) UpdateEnv(ctx context.Context, serviceUUID string, req *models.ServiceEnvironmentVariableUpdateRequest) (*models.ServiceEnvironmentVariable, error) {
var env models.ServiceEnvironmentVariable
err := s.client.Patch(ctx, fmt.Sprintf("services/%s/envs", serviceUUID), req, &env)
if err != nil {
return nil, fmt.Errorf("failed to update environment variable for service %s: %w", serviceUUID, err)
@@ -157,11 +157,11 @@ func (s *Service) DeleteEnv(ctx context.Context, serviceUUID, envUUID string) er
}
// BulkUpdateEnvs updates multiple environment variables in a single request
func (s *Service) BulkUpdateEnvs(ctx context.Context, serviceUUID string, req *BulkUpdateEnvsRequest) (*BulkUpdateEnvsResponse, error) {
var response BulkUpdateEnvsResponse
func (s *Service) BulkUpdateEnvs(ctx context.Context, serviceUUID string, req *models.ServiceEnvBulkUpdateRequest) (models.ServiceEnvBulkUpdateResponse, error) {
var response models.ServiceEnvBulkUpdateResponse
err := s.client.Patch(ctx, fmt.Sprintf("services/%s/envs/bulk", serviceUUID), req, &response)
if err != nil {
return nil, fmt.Errorf("failed to bulk update environment variables for service %s: %w", serviceUUID, err)
}
return &response, nil
return response, nil
}
+89 -8
View File
@@ -255,14 +255,14 @@ func TestService_ListEnvs(t *testing.T) {
"uuid": "env-1",
"key": "DATABASE_URL",
"value": "postgres://localhost",
"is_build_time": false,
"is_buildtime": false,
"is_preview": false
},
{
"uuid": "env-2",
"key": "API_KEY",
"value": "secret",
"is_build_time": true,
"is_buildtime": true,
"is_preview": false
}
]`))
@@ -290,7 +290,7 @@ func TestService_CreateEnv(t *testing.T) {
"uuid": "env-new",
"key": "NEW_VAR",
"value": "new_value",
"is_build_time": false,
"is_buildtime": false,
"is_preview": false
}`))
}))
@@ -299,7 +299,7 @@ func TestService_CreateEnv(t *testing.T) {
client := api.NewClient(server.URL, "test-token")
svc := NewService(client)
env, err := svc.CreateEnv(context.Background(), "service-uuid-123", &models.EnvironmentVariableCreateRequest{
env, err := svc.CreateEnv(context.Background(), "service-uuid-123", &models.ServiceEnvironmentVariableCreateRequest{
Key: "NEW_VAR",
Value: "new_value",
})
@@ -319,7 +319,7 @@ func TestService_UpdateEnv(t *testing.T) {
"uuid": "env-123",
"key": "UPDATED_VAR",
"value": "updated_value",
"is_build_time": true,
"is_buildtime": true,
"is_preview": false
}`))
}))
@@ -329,9 +329,8 @@ func TestService_UpdateEnv(t *testing.T) {
svc := NewService(client)
newKey := "UPDATED_VAR"
env, err := svc.UpdateEnv(context.Background(), "service-uuid-123", &models.EnvironmentVariableUpdateRequest{
UUID: "env-123",
Key: &newKey,
env, err := svc.UpdateEnv(context.Background(), "service-uuid-123", &models.ServiceEnvironmentVariableUpdateRequest{
Key: &newKey,
})
require.NoError(t, err)
@@ -354,3 +353,85 @@ func TestService_DeleteEnv(t *testing.T) {
require.NoError(t, err)
}
func TestService_Create(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/services", r.URL.Path)
assert.Equal(t, "POST", r.Method)
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{
"uuid": "service-new-uuid",
"name": "WordPress",
"status": "starting"
}`))
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewService(client)
name := "My WordPress"
service, err := svc.Create(context.Background(), &models.ServiceCreateRequest{
Type: "wordpress-with-mysql",
ServerUUID: "server-uuid",
ProjectUUID: "project-uuid",
EnvironmentName: "production",
Name: &name,
})
require.NoError(t, err)
assert.Equal(t, "service-new-uuid", service.UUID)
assert.Equal(t, "WordPress", service.Name)
}
func TestService_Create_WithInstantDeploy(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/services", r.URL.Path)
assert.Equal(t, "POST", r.Method)
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{
"uuid": "service-instant-uuid",
"name": "Ghost Blog",
"status": "running"
}`))
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewService(client)
instantDeploy := true
service, err := svc.Create(context.Background(), &models.ServiceCreateRequest{
Type: "ghost",
ServerUUID: "server-uuid",
ProjectUUID: "project-uuid",
EnvironmentName: "production",
InstantDeploy: &instantDeploy,
})
require.NoError(t, err)
assert.Equal(t, "service-instant-uuid", service.UUID)
assert.Equal(t, "Ghost Blog", service.Name)
}
func TestService_Create_Error(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"error": "invalid service type"}`))
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewService(client)
_, err := svc.Create(context.Background(), &models.ServiceCreateRequest{
Type: "invalid-type",
ServerUUID: "server-uuid",
ProjectUUID: "project-uuid",
EnvironmentName: "production",
})
require.Error(t, err)
}
+41 -43
View File
@@ -5,92 +5,90 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sort"
"time"
compareVersion "github.com/hashicorp/go-version"
"github.com/spf13/viper"
)
// Version variables injected by GoReleaser at build time via ldflags
var (
version = "v1.0.3"
version = "v1.4.0"
)
// GitHubAPIURL is the URL for fetching CLI version tags (exported for testing)
var GitHubAPIURL = "https://api.github.com/repos/coollabsio/coolify-cli/git/refs/tags"
func GetVersion() string {
return version
}
// CheckInterval for version checking
const CheckInterval = 10 * time.Minute
// Tag represents a git tag for version checking
type Tag struct {
Ref string `json:"ref"`
}
// CheckLatestVersionOfCli checks for CLI updates
func CheckLatestVersionOfCli(debug bool) (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 GetVersion(), nil
}
}
// CheckLatestVersionOfCli checks for CLI updates on every command.
// Errors are handled silently - the function returns without printing anything
// if the GitHub API call fails.
func CheckLatestVersionOfCli(_ bool) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Update check time
viper.Set("lastupdatechecktime", time.Now().Format(time.RFC3339))
if err := viper.WriteConfig(); err != nil {
log.Printf("Failed to write config: %v\n", err)
}
url := "https://api.github.com/repos/coollabsio/coolify-cli/git/refs/tags"
ctx := context.Background()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
req, err := http.NewRequestWithContext(ctx, "GET", GitHubAPIURL, nil)
if err != nil {
return "", err
return "", nil // Silent fail
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
return "", nil // Silent fail
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
if resp.StatusCode != 200 {
return "", nil // Silent fail
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("%d - Failed to fetch data from %s. Error: %s", resp.StatusCode, url, string(body))
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil // Silent fail
}
var tags []Tag
if err := json.Unmarshal(body, &tags); err != nil {
return "", err
return "", nil // Silent fail
}
if len(tags) == 0 {
return "", nil // Silent fail
}
versionsRaw := make([]string, 0, len(tags))
for _, tag := range tags {
versionStr := tag.Ref[10:]
versionsRaw = append(versionsRaw, versionStr)
if len(tag.Ref) > 10 {
versionStr := tag.Ref[10:]
versionsRaw = append(versionsRaw, versionStr)
}
}
versions := make([]*compareVersion.Version, len(versionsRaw))
for i, raw := range versionsRaw {
if len(versionsRaw) == 0 {
return "", nil // Silent fail
}
versions := make([]*compareVersion.Version, 0, len(versionsRaw))
for _, raw := range versionsRaw {
v, err := compareVersion.NewVersion(raw)
if err != nil {
return "", err
continue // Skip invalid versions
}
versions[i] = v
versions = append(versions, v)
}
if len(versions) == 0 {
return "", nil // Silent fail
}
sort.Sort(compareVersion.Collection(versions))
@@ -99,11 +97,11 @@ func CheckLatestVersionOfCli(debug bool) (string, error) {
// Compare versions properly using semantic versioning
currentVersion, err := compareVersion.NewVersion(GetVersion())
if err != nil {
return latestVersion.String(), err
return "", nil // Silent fail
}
if latestVersion.GreaterThan(currentVersion) {
fmt.Printf("There is a new version of Coolify CLI available.\nPlease update with 'coolify update'.\n\n")
fmt.Printf("A new version (%s) is available. Update with: coolify update\n", latestVersion.String())
}
return latestVersion.String(), nil
}
+262
View File
@@ -0,0 +1,262 @@
package version
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
)
func TestGetVersion(t *testing.T) {
v := GetVersion()
if v == "" {
t.Error("GetVersion() returned empty string")
}
// Version should start with 'v'
if v[0] != 'v' {
t.Errorf("GetVersion() = %q, expected to start with 'v'", v)
}
}
func TestCheckLatestVersionOfCli_UpdateAvailable(t *testing.T) {
// Save original values
originalURL := GitHubAPIURL
originalVersion := version
defer func() {
GitHubAPIURL = originalURL
version = originalVersion
}()
// Set a low version to ensure update is available
version = "v0.0.1"
// Create mock server with newer version
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
// Return tags in GitHub API format
_, _ = w.Write([]byte(`[{"ref":"refs/tags/v1.0.0"},{"ref":"refs/tags/v2.0.0"}]`))
}))
defer server.Close()
GitHubAPIURL = server.URL
// Capture stdout to check for update message
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
latestVersion, err := CheckLatestVersionOfCli(false)
_ = w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil", err)
}
if latestVersion != "2.0.0" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want %q", latestVersion, "2.0.0")
}
// Should print update message
expectedMsg := "A new version (2.0.0) is available. Update with: coolify update\n"
if output != expectedMsg {
t.Errorf("CheckLatestVersionOfCli() output = %q, want %q", output, expectedMsg)
}
}
func TestCheckLatestVersionOfCli_NoUpdate(t *testing.T) {
// Save original values
originalURL := GitHubAPIURL
originalVersion := version
defer func() {
GitHubAPIURL = originalURL
version = originalVersion
}()
// Set a high version to ensure no update is available
version = "v99.99.99"
// Create mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`[{"ref":"refs/tags/v1.0.0"},{"ref":"refs/tags/v2.0.0"}]`))
}))
defer server.Close()
GitHubAPIURL = server.URL
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
latestVersion, err := CheckLatestVersionOfCli(false)
_ = w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil", err)
}
// Function returns the latest version from GitHub (2.0.0), not the current version
if latestVersion != "2.0.0" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want %q", latestVersion, "2.0.0")
}
// Should NOT print any message when already on latest (current v99.99.99 > latest v2.0.0)
if output != "" {
t.Errorf("CheckLatestVersionOfCli() should not print anything when on latest version, got: %q", output)
}
}
func TestCheckLatestVersionOfCli_APIError_SilentFail(t *testing.T) {
// Save original URL
originalURL := GitHubAPIURL
defer func() {
GitHubAPIURL = originalURL
}()
// Create mock server that returns error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"error": "internal server error"}`))
}))
defer server.Close()
GitHubAPIURL = server.URL
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
latestVersion, err := CheckLatestVersionOfCli(false)
_ = w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
// Should return empty string and nil error (silent fail)
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on API error", err)
}
if latestVersion != "" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on API error", latestVersion)
}
// Should NOT print anything on error
if output != "" {
t.Errorf("CheckLatestVersionOfCli() should not print anything on API error, got: %q", output)
}
}
func TestCheckLatestVersionOfCli_NetworkError_SilentFail(t *testing.T) {
// Save original URL
originalURL := GitHubAPIURL
defer func() {
GitHubAPIURL = originalURL
}()
// Use invalid URL to cause network error
GitHubAPIURL = "http://localhost:1" // Port 1 should fail to connect
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
latestVersion, err := CheckLatestVersionOfCli(false)
_ = w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
// Should return empty string and nil error (silent fail)
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on network error", err)
}
if latestVersion != "" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on network error", latestVersion)
}
// Should NOT print anything on error
if output != "" {
t.Errorf("CheckLatestVersionOfCli() should not print anything on network error, got: %q", output)
}
}
func TestCheckLatestVersionOfCli_InvalidJSON_SilentFail(t *testing.T) {
// Save original URL
originalURL := GitHubAPIURL
defer func() {
GitHubAPIURL = originalURL
}()
// Create mock server that returns invalid JSON
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`not valid json`))
}))
defer server.Close()
GitHubAPIURL = server.URL
latestVersion, err := CheckLatestVersionOfCli(false)
// Should return empty string and nil error (silent fail)
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on invalid JSON", err)
}
if latestVersion != "" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on invalid JSON", latestVersion)
}
}
func TestCheckLatestVersionOfCli_EmptyTags_SilentFail(t *testing.T) {
// Save original URL
originalURL := GitHubAPIURL
defer func() {
GitHubAPIURL = originalURL
}()
// Create mock server that returns empty array
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`[]`))
}))
defer server.Close()
GitHubAPIURL = server.URL
latestVersion, err := CheckLatestVersionOfCli(false)
// Should return empty string and nil error (silent fail)
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on empty tags", err)
}
if latestVersion != "" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on empty tags", latestVersion)
}
}
+411
View File
@@ -0,0 +1,411 @@
# Coolify CLI Installer for Windows
# This script installs the coolify-cli from GitHub releases
# Supports Windows on amd64/arm64 architectures
#Requires -Version 5.1
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[string]$Version = "",
[Parameter()]
[switch]$User,
[Parameter()]
[switch]$Help,
[Parameter()]
[string]$InstallDir = ""
)
# Support environment variables for web-based installation
if (-not $Version -and $env:COOLIFY_VERSION) {
$Version = $env:COOLIFY_VERSION
}
if (-not $User.IsPresent -and $env:COOLIFY_USER_INSTALL) {
$User = [bool]($env:COOLIFY_USER_INSTALL -match '^(1|true|yes)$')
}
if (-not $InstallDir -and $env:COOLIFY_INSTALL_DIR) {
$InstallDir = $env:COOLIFY_INSTALL_DIR
}
# Configuration
$Script:REPO = "coollabsio/coolify-cli"
$Script:BINARY_NAME = "coolify.exe"
$Script:GLOBAL_INSTALL_DIR = "$env:ProgramFiles\Coolify"
$Script:USER_INSTALL_DIR = "$env:LOCALAPPDATA\Coolify"
$Script:TEMP_FILE = ""
# Error action preference
$ErrorActionPreference = "Stop"
# Cleanup function
function Cleanup {
if ($Script:TEMP_FILE -and (Test-Path $Script:TEMP_FILE)) {
Remove-Item -Path $Script:TEMP_FILE -Force -ErrorAction SilentlyContinue
}
}
# Register cleanup on exit
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action { Cleanup } | Out-Null
# Show help
function Show-Help {
Write-Host @"
Coolify CLI Installer for Windows
Usage: .\install.ps1 [OPTIONS] [VERSION]
OPTIONS:
-User Install to user directory (no admin rights required)
-InstallDir Custom installation directory
-Help Show this help message
ARGUMENTS:
VERSION Specific version to install (e.g., v1.0.0)
If not specified, installs the latest release
EXAMPLES:
.\install.ps1 Install latest version to Program Files
.\install.ps1 -User Install latest version to user directory
.\install.ps1 v1.0.0 Install specific version to Program Files
.\install.ps1 -User v1.0.0 Install specific version to user directory
.\install.ps1 -InstallDir "C:\Tools" Install to custom directory
NOTES:
- Administrator privileges are required for global installation
- User installation does not require admin rights
- The installation directory will be added to PATH if not already present
"@
exit 0
}
# Write colored output
function Write-ColorOutput {
param(
[string]$Message,
[string]$ForegroundColor = "White"
)
Write-Host $Message -ForegroundColor $ForegroundColor
}
function Write-Success {
param([string]$Message)
Write-ColorOutput $Message -ForegroundColor Green
}
function Write-Warning {
param([string]$Message)
Write-ColorOutput $Message -ForegroundColor Yellow
}
function Write-ErrorMessage {
param([string]$Message)
Write-ColorOutput $Message -ForegroundColor Red
}
# Error handler
function Stop-WithError {
param([string]$Message)
Write-ErrorMessage "Error: $Message"
Cleanup
throw $Message
}
# Check if running as administrator
function Test-Administrator {
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
# Detect platform and architecture
function Get-PlatformInfo {
$os = "windows"
# Detect architecture
$arch = switch ($env:PROCESSOR_ARCHITECTURE) {
"AMD64" { "amd64" }
"ARM64" { "arm64" }
"x86" { Stop-WithError "32-bit Windows is not supported" }
default { Stop-WithError "Unsupported architecture: $env:PROCESSOR_ARCHITECTURE" }
}
return @{
OS = $os
Arch = $arch
}
}
# Fetch latest release version from GitHub
function Get-LatestVersion {
Write-Host "Fetching latest release version..."
try {
$response = Invoke-RestMethod -Uri "https://api.github.com/repos/$Script:REPO/releases/latest" -Method Get
$latestVersion = $response.tag_name
if (-not $latestVersion) {
Stop-WithError "Failed to fetch latest release version from GitHub"
}
return $latestVersion
}
catch {
Stop-WithError "Failed to fetch latest release version: $($_.Exception.Message)"
}
}
# Validate version format
function Test-VersionFormat {
param([string]$VersionString)
# Check if version matches semantic versioning (with or without 'v' prefix)
if ($VersionString -notmatch '^v?\d+\.\d+\.\d+(-.*)?$') {
Stop-WithError "Invalid version format: $VersionString`nExpected format: v1.0.0 or 1.0.0"
}
# Ensure version starts with 'v' for GitHub releases
if ($VersionString -notmatch '^v') {
$VersionString = "v$VersionString"
}
return $VersionString
}
# Check if coolify is already installed
function Test-ExistingInstallation {
param([string]$NewVersion)
try {
$coolifyCmd = Get-Command coolify -ErrorAction SilentlyContinue
if ($coolifyCmd) {
$currentVersion = & coolify version 2>$null | Select-Object -First 1
if (-not $currentVersion) {
$currentVersion = "unknown"
}
Write-Warning "Coolify CLI is already installed: $currentVersion"
Write-Host "This will upgrade/reinstall to version " -NoNewline
Write-Success $NewVersion
$response = Read-Host "Continue? [y/N]"
if ($response -notmatch '^[Yy]$') {
Write-Host "Installation cancelled."
exit 0
}
}
}
catch {
# Coolify not found, continue with installation
}
}
# Download and install binary from GitHub release
function Install-FromGitHub {
param(
[string]$Repo,
[string]$Release,
[string]$Name,
[string]$InstallDirectory,
[hashtable]$Platform
)
# Clean version (remove 'v' prefix if present)
$cleanVersion = $Release -replace '^v', ''
$filename = "${Name}_${cleanVersion}_$($Platform.OS)_$($Platform.Arch).zip"
$downloadUrl = "https://github.com/${Repo}/releases/download/${Release}/${filename}"
Write-Success "Downloading $Name $Release"
Write-Host "Platform: $($Platform.OS)/$($Platform.Arch)"
Write-Host "URL: $downloadUrl"
# Create temp file
$Script:TEMP_FILE = [System.IO.Path]::GetTempFileName() + ".zip"
# Download file
try {
$ProgressPreference = 'SilentlyContinue' # Speed up download
Invoke-WebRequest -Uri $downloadUrl -OutFile $Script:TEMP_FILE -ErrorAction Stop
$ProgressPreference = 'Continue'
}
catch {
Stop-WithError "Failed to download from ${downloadUrl}`nPlease check if the version exists or try again later.`nError: $($_.Exception.Message)"
}
# Verify downloaded file is not empty
if ((Get-Item $Script:TEMP_FILE).Length -eq 0) {
Stop-WithError "Downloaded file is empty"
}
# Create install directory if it doesn't exist
if (-not (Test-Path $InstallDirectory)) {
Write-Host "Creating directory: $InstallDirectory"
try {
New-Item -ItemType Directory -Path $InstallDirectory -Force | Out-Null
}
catch {
Stop-WithError "Failed to create directory $InstallDirectory : $($_.Exception.Message)"
}
}
# Extract binary
Write-Host "Installing $Name to $InstallDirectory\$Script:BINARY_NAME"
try {
# Remove existing binary if present
$binaryPath = Join-Path $InstallDirectory $Script:BINARY_NAME
if (Test-Path $binaryPath) {
Remove-Item -Path $binaryPath -Force
}
# Extract zip file
Expand-Archive -Path $Script:TEMP_FILE -DestinationPath $InstallDirectory -Force
}
catch {
Stop-WithError "Failed to extract binary: $($_.Exception.Message)"
}
# Verify installation
$installedBinary = Join-Path $InstallDirectory $Script:BINARY_NAME
if (-not (Test-Path $installedBinary)) {
Stop-WithError "Binary was not installed to $installedBinary"
}
Write-Success "$Name installed successfully to $installedBinary"
# Add to PATH if not already present
Add-ToPath -Directory $InstallDirectory
# Show installed version
try {
$installedVersion = & $installedBinary version 2>$null | Select-Object -First 1
if ($installedVersion) {
Write-Host "Installed version: " -NoNewline
Write-Success $installedVersion
}
}
catch {
# Version check failed, but installation was successful
}
}
# Add directory to PATH
function Add-ToPath {
param([string]$Directory)
# Determine which PATH to modify (user or system)
$pathScope = if ($User -or -not (Test-Administrator)) { "User" } else { "Machine" }
# Get current PATH
$currentPath = [Environment]::GetEnvironmentVariable("Path", $pathScope)
# Check if directory is already in PATH
$pathDirs = $currentPath -split ';' | ForEach-Object { $_.Trim() }
if ($pathDirs -contains $Directory) {
Write-Host "Directory is already in PATH"
return
}
Write-Host "Adding $Directory to PATH ($pathScope)..."
try {
# Add to PATH
$newPath = "$currentPath;$Directory"
[Environment]::SetEnvironmentVariable("Path", $newPath, $pathScope)
# Update current session PATH
$env:Path = "$env:Path;$Directory"
Write-Success "✓ Added to PATH. You may need to restart your terminal for changes to take effect."
}
catch {
Write-Warning "Failed to add to PATH automatically: $($_.Exception.Message)"
Write-Host "Please add the following directory to your PATH manually:"
Write-Host " $Directory"
}
}
# Main installation flow
function Install-CoolifyCLI {
Write-Host "Coolify CLI Installer for Windows"
Write-Host "=================================="
Write-Host ""
# Show help if requested
if ($Help) {
Show-Help
}
# Detect platform
$platform = Get-PlatformInfo
# Determine version to install
$versionToInstall = if ($Version) {
Test-VersionFormat -VersionString $Version
} else {
Get-LatestVersion
}
Write-Host "Version to install: $versionToInstall"
Write-Host ""
# Check existing installation
Test-ExistingInstallation -NewVersion $versionToInstall
# Determine install directory
$installDirectory = if ($InstallDir) {
$InstallDir
} elseif ($User) {
$Script:USER_INSTALL_DIR
} else {
$Script:GLOBAL_INSTALL_DIR
}
# Check for admin rights if installing globally
if (-not $User -and -not $InstallDir -and -not (Test-Administrator)) {
Write-Warning "Global installation requires administrator privileges."
Write-Host "Please run this script as Administrator, or use -User flag for user installation."
Write-Host ""
$response = Read-Host "Switch to user installation? [Y/n]"
if ($response -match '^[Nn]$') {
Stop-WithError "Installation cancelled. Please run as Administrator for global installation."
}
$installDirectory = $Script:USER_INSTALL_DIR
}
$installMode = if ($User -or $installDirectory -eq $Script:USER_INSTALL_DIR) {
"User (no admin rights required)"
} else {
"Global (administrator)"
}
Write-Host "Install mode: $installMode"
Write-Host "Install directory: $installDirectory"
Write-Host ""
# Download and install
Install-FromGitHub -Repo $Script:REPO -Release $versionToInstall -Name "coolify-cli" -InstallDirectory $installDirectory -Platform $platform
Write-Host ""
Write-Success "Installation complete!"
Write-Host "Run 'coolify --help' to get started"
Write-Host ""
Write-Host "Note: If 'coolify' command is not found, please restart your terminal."
}
# Run main installation
try {
Install-CoolifyCLI
}
catch {
Write-ErrorMessage "Unexpected error: $($_.Exception.Message)"
Write-ErrorMessage $_.ScriptStackTrace
Cleanup
}
finally {
Cleanup
}
+1 -1
View File
@@ -173,7 +173,7 @@ download_from_github() {
local name=$3
local install_dir=$4
local filename="${name}_${release}_${OS}_${ARCH}.tar.gz"
local filename="${name}_${release#v}_${OS}_${ARCH}.tar.gz"
local download_url="https://github.com/${repo}/releases/download/${release}/${filename}"
echo -e "${GREEN}Downloading ${name} ${release}${NC}"
+12
View File
@@ -0,0 +1,12 @@
{
"uuid": "env-test-uuid-123",
"key": "DATABASE_URL",
"value": "postgres://localhost/mydb",
"real_value": "postgres://user:pass@localhost/mydb",
"is_buildtime": true,
"is_preview": false,
"is_literal": false,
"is_shown_once": false,
"is_runtime": true,
"is_shared": false
}