Compare commits

...

136 Commits

Author SHA1 Message Date
Andras Bacsai c45a0b35ec feat(service): wire format and show-sensitive flags to get and list commands 2026-04-16 11:30:44 +02:00
Andras Bacsai 5e2b3d08db feat(docs): generate quick llms.txt and full llms-full.txt
Refactor `coolify docs llms` to emit two AI-oriented artifacts:
- `llms.txt` as a concise operating guide
- `llms-full.txt` as the exhaustive command and flag catalog

Update tests to cover quick/full generation, document both files in README,
and adjust CI to fail when either generated file is out of date.
2026-03-31 17:44:24 +02:00
github-actions[bot] b0eb8dbd15 chore: bump version to v1.6.0 2026-03-30 13:06:16 +00:00
Andras Bacsai c292ba8b42 test(deployment): use assert.NoError in deploy service test
Replace `require.NoError` with `assert.NoError` when reading request body in `deployment_test` to keep assertion style consistent in this test block.
2026-03-30 14:10:34 +02:00
Andras Bacsai 4ae6065ecf Merge pull request #67 from toanalien/feature/add-llms-config
feat(docs): enhance llms.txt with overview, data models, short flags, and defaults
2026-03-30 14:03:36 +02:00
Andras Bacsai 80bc511fd8 feat(docs): derive llms aliases from command tree
Refactor `docs` generation to split intro/body content and build the
`Command Aliases` section dynamically from Cobra commands instead of a
hardcoded list.

Add `cmd/docs_test.go` coverage for:
- alias extraction from nested command trees
- flag rendering with shorthand and default values

Regenerate `llms.txt` to reflect the new alias output and updated docs.
2026-03-30 13:58:54 +02:00
Andras Bacsai b2da3013d2 Merge remote-tracking branch 'origin/v4.x' into feature/add-llms-config 2026-03-30 13:54:32 +02:00
Andras Bacsai 28d54b0df9 feat(deployment): support preview deploy fields via request payload
Add shared deploy flags (`--force`, `--pull-request-id`, `--docker-tag`) to
`deploy uuid|name|batch`, and validate `--docker-tag` against minimum
Coolify version `4.0.0-beta.471`.

Refactor deployment triggering to send a structured `DeployRequest` via
`POST /deploy` instead of GET query parameters, enabling optional payload
fields for preview/tagged deployments.

Update deployment service tests to assert POST method and JSON body, and
document the new flags and example usage in the README.
2026-03-30 13:50:52 +02:00
Andras Bacsai c6378a8280 Merge pull request #68 from coollabsio/66-investigate-json-output-breakage
fix(version): write update notices to stderr for JSON-safe output
2026-03-30 13:36:03 +02:00
Andras Bacsai ce0e8fe9cd fix(version): write update notice to stderr to preserve JSON stdout
Redirect the version update message from stdout to stderr so command JSON output stays machine-readable.

Expand checker tests with shared stdout/stderr capture helpers and assertions to verify:
- update notices are emitted on stderr only
- stdout remains clean during checks and errors
- JSON output to stdout is unaffected when an update is available
2026-03-30 13:35:01 +02:00
toanalien 528b1359aa feat(docs): enhance llms.txt with overview, data models, short flags, and defaults
Add comprehensive header to llms.txt including installation, authentication,
configuration, command aliases, usage examples, API notes, and data model
documentation. Update docs generator to output short flags and default values.
2026-03-27 13:48:40 +07:00
Andras Bacsai eabce9a8e1 feat(homebrew): add Homebrew tap support for CLI distribution
Add Homebrew formula configuration to goreleaser for distributing
coolify-cli via the homebrew-coolify-cli tap. This enables macOS/Linux
users to install the CLI with:

  brew install coollabsio/coolify-cli/coolify-cli

Changes include goreleaser formula config, the HOMEBREW_TAP_GITHUB_TOKEN
in the release workflow, and updated installation instructions in README.
2026-03-23 15:54:17 +01:00
Andras Bacsai f43cd16f6f Merge pull request #64 from coollabsio/coolify-cli-storage-endpoints
feat(storage): add CRUD operations for persistent and file storages
2026-03-23 14:44:30 +01:00
Andras Bacsai e49daeea95 Merge pull request #65 from coollabsio/62-app-env-update
feat(env): allow updating vars by UUID or key identifier
2026-03-23 14:44:19 +01:00
Andras Bacsai ccf578e537 docs(env): reorder parameters in env update commands
Reorganize parameter ordering for consistency across app, database, and
service env update commands. Move string parameters (--key, --value) after
boolean parameters. Simplify --key parameter description.
2026-03-23 14:41:03 +01:00
Andras Bacsai cad379eefb test(service): use assert.True/False helpers for boolean assertions
Replace assert.Equal with more specific boolean assertion helpers
(assert.True and assert.False) for improved readability and
idiomatic Go testing practices.
2026-03-23 14:40:55 +01:00
Andras Bacsai 53ab7b315c feat(storage): require minimum API version 4.0.0-beta.470
Add version check validation across all storage CRUD operations in application,
database, and service commands. This ensures the API client meets the minimum
version requirement before executing storage operations.

Also includes:
- Documentation updates for dockerfile-target-build parameter in llms.txt
- Field alignment formatting fixes in ApplicationCreateRequest structs
2026-03-23 14:38:19 +01:00
Andras Bacsai 8a7d2c20af Merge remote-tracking branch 'origin/v4.x' into coolify-cli-storage-endpoints 2026-03-23 14:32:49 +01:00
Andras Bacsai fcd1a01fb7 Merge remote-tracking branch 'origin/v4.x' into 62-app-env-update 2026-03-23 14:32:48 +01:00
Andras Bacsai f67411de2c feat(storage): add CRUD operations for persistent and file storages
Add comprehensive storage management system for applications, databases, and services:

- Implement storage subcommands (list, create, update, delete) with full API integration
- Add support for both persistent volumes and file-based storage management
- Create Storage model with comprehensive validation for type-specific operations
- Implement ApplicationService, DatabaseService, and ServiceService storage methods
- Add extensive unit tests covering CRUD operations and edge cases
- Integrate storage subcommand into application, database, and service CLI commands
- Add dockerfile-target-build flag support to application creation and update commands

Storage operations support:
- Persistent volumes: create with optional host paths, update mount paths and names
- File storages: create/update with content or file system paths, support directories
- Common features: mount path management, read-only detection, preview suffix toggling
2026-03-23 14:27:21 +01:00
Andras Bacsai 146ce7a7b0 feat(env): allow updating vars by UUID or key identifier
Add support for identifying environment variables by UUID or key name in
update commands, eliminating the requirement to use --key for lookups.
Commands now accept <env_uuid_or_key> as a positional argument to identify
the variable, with --key becoming optional for renaming only. Applied to
app, database, and service env update commands and updated documentation.
2026-03-23 11:16:47 +01:00
Andras Bacsai b661576fc1 Merge pull request #63 from coollabsio/61-expand-application-struct
feat(models): expand Application with extended configuration
2026-03-23 11:07:57 +01:00
Andras Bacsai 0872e48283 feat(models): add ApplicationSettings and expand Application with extended configuration
- Add ApplicationSettings struct for application-level feature flags
- Expand Application model with git, build, health check, and resource limit fields
- Add comprehensive unit tests for Application marshaling/unmarshaling
- Add application.json fixture for testing
2026-03-23 11:03:01 +01:00
Andras Bacsai 303fad333b Merge pull request #61 from Dagnan/fix_format_json_for_database_get_list
fix(database): respect --format flag in database list and get commands
2026-03-23 10:55:28 +01:00
Andras Bacsai 8ee7ec4c0d refactor(docs): use fmt.Fprintf and restrict file permissions to 0600
- Replace sb.WriteString(fmt.Sprintf(...)) with fmt.Fprintf for more idiomatic
  and efficient string formatting
- Tighten output file permissions from 0644 to 0600
- Move pflag to direct dependencies as it's now explicitly used
2026-03-23 10:46:11 +01:00
Andras Bacsai 98f40f03dc Merge pull request #52 from toanalien/feature/add-llms-config
feat: Add llms.txt for AI agent command specification
2026-03-23 10:31:48 +01:00
Andras Bacsai 28521a2ca0 ci: add llms.txt validation workflow job
Adds a new 'llms-txt' job to the test workflow that regenerates llms.txt
and validates it hasn't changed, ensuring the documentation stays in sync
with the CLI implementation.
2026-03-23 10:31:22 +01:00
Andras Bacsai dd4b271faf feat(docs): add llms command to generate machine-readable CLI spec
Add new `docs llms` subcommand that generates a machine-readable llms.txt
file defining all CLI commands and parameters. This enables AI agents to
understand and interact with the CLI programmatically.

- Implement writeLLMsCommand() to recursively document command hierarchy
- Regenerate llms.txt using the new generator for consistency
- Add --output flag to customize the output file path
2026-03-23 10:29:38 +01:00
Andras Bacsai cdc5a1e732 Merge pull request #50 from YaRissi/fix/app-env-sync
fix: app env bulk
2026-03-23 10:28:15 +01:00
Andras Bacsai 7e3639b41a Merge remote-tracking branch 'origin/v4.x' into feature/add-llms-config 2026-03-23 10:24:12 +01:00
Andras Bacsai 6bd783dc8a Merge remote-tracking branch 'origin/v4.x' into v4.x 2026-03-23 10:21:46 +01:00
Andras Bacsai 2ac1d0f869 chore: bump version to v1.5.0 2026-03-23 10:21:28 +01:00
Michel Pigassou f4c4c962ff fix(database): respect --format flag in list and get commands
The database list and get commands were hardcoded to use table format,
ignoring the global --format flag. This prevented users from using
--format=json or --format=pretty output formats.

Changes:
- cmd/database/list.go: read format flag from command
- cmd/database/get.go: read format flag from command
- Both commands now follow the same pattern as other list/get commands
2026-03-22 13:54:34 +01:00
Andras Bacsai 801c2e0b3c Merge pull request #55 from baer95/tablewriter
refactor: improve table output format
2026-03-20 18:14:18 +01:00
Andras Bacsai ea4bec7492 refactor(output): reorganize table formatter and improve test robustness
- Move empty slice check before table writer creation for early exit
- Fix error message typo: "ascii w" → "table"
- Enhance boolean test to dynamically locate data rows instead of hardcoded indices
2026-03-20 18:14:04 +01:00
Andras Bacsai 7e59cd76c3 feat(env): add database environment variable management
Add complete environment variable management for databases with full CRUD
operations, including a new sync command to load variables from .env files.
Support comment field across application, service, and database entities.

- Implement database env service layer with create, read, update, delete, list
- Add sync command for bulk loading and updating from .env files
- Add --comment flag to application and service env commands
- Include comprehensive test coverage for database service operations
- Update to Go 1.24.13
2026-03-20 17:31:42 +01:00
Andras Bacsai 81b9e9cdd0 test(application): fix linter warnings in BulkUpdateEnvs tests
Address unused function parameters and unhandled error return values.
2026-03-19 22:26:15 +01:00
Andras Bacsai fe01e8f9b8 test(application): verify BulkUpdateEnvs request serialization and error handling
- Add request body assertions to validate correct serialization in BulkUpdateEnvs test
- Add new test case for API error handling (500 response)
2026-03-19 22:08:26 +01:00
Andras Bacsai daa2a4cdcb Merge remote-tracking branch 'origin/v4.x' into fix/app-env-sync 2026-03-19 22:06:32 +01:00
Andras Bacsai 1703fd2e52 Merge pull request #60 from coollabsio/next
Next
2026-03-19 22:04:08 +01:00
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
Bernhard Frick 333ff3c504 refactor: improve table output format
replace text/tabwriter with olekukonko/tablewriter for nicer rendering of output formatted as tables
2026-01-26 21:52:03 +01:00
toanalien 0daae657fb feat: Add llms.txt for AI agent command specification
Introduces a machine-readable llms.txt file that defines all CLI commands and their parameters. This file is generated by analyzing the cobra command definitions and is intended to enable AI agents to understand and interact with the CLI.
2025-12-26 16:17:00 +07: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
YaRissi ea3236672b fixing app env sync 2025-12-19 18:09:25 +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
Andras Bacsai 63a882107a Merge pull request #37 from coollabsio/add-deployment-logs-cli
Add deployment management commands for improved user experience
2025-11-10 14:21:49 +01:00
Andras Bacsai 08cd3b8ac7 fix: resolve nilerr linting error in deployment logs pretty-print logic
Restructure JSON pretty-print logic to check for success instead of
failure, avoiding nilerr linting violation while maintaining fallback
behavior. The change ensures proper error handling patterns without
hiding potential bugs.
2025-11-10 14:19:29 +01:00
Andras Bacsai 06f191e9ba Merge branch 'v4.x' into add-deployment-logs-cli 2025-11-10 14:18:09 +01:00
Andras Bacsai 7a19a02c02 fix: resolve remaining nilerr linting errors
- Explicitly ignore errors where we have intentional fallback behavior
- Use blank identifier (_) for errors we don't need to check
- Restructure error checking to avoid nilerr pattern
- All errors are properly handled or explicitly ignored
2025-11-10 13:55:10 +01:00
Andras Bacsai 806a6b9716 fix: resolve linting errors (errcheck, nilerr, revive, gci)
- Add error checking for w.Write calls in tests
- Rename unused http.Request parameters to _ in test handlers
- Fix nilerr errors by using different variable names to avoid shadowing
- Fix gci import grouping (stdlib, external, internal)
2025-11-10 13:48:58 +01:00
Andras Bacsai a18d751ad4 style: fix gofmt formatting in deployment_test.go 2025-11-10 13:44:42 +01:00
Andras Bacsai 9283717821 fix: add --format flag support and filter hidden logs in JSON output
Add proper support for --format flag (json/pretty/table) in deployment logs:
- Respect global --format flag for output formatting
- Filter hidden logs by default in JSON/pretty formats
- Only show debug logs when --debuglogs flag is present
- Apply --lines limit only to table format (JSON shows complete data)

Changes:
- Add GetLogsByDeploymentWithFormat() and GetLogsByApplicationWithFormat()
- Filter log entries with "hidden": true unless --debuglogs is set
- Return raw JSON for --format json
- Return pretty-printed JSON for --format pretty
- Return human-readable text for --format table (default)
2025-11-10 13:26:29 +01:00
Andras Bacsai a44c712163 feat: add deployment logs commands with human-readable output
Add new commands to query and display deployment logs:
- coolify app deployments list <app-uuid>: List all deployments for an application
- coolify app deployments logs <app-uuid> [deployment-uuid>]: Get deployment logs

Features:
- Retrieves latest deployment logs by default or specific deployment by UUID
- Parses JSON log format into human-readable line-by-line output
- Supports --lines flag to limit output to last N lines
- Supports --follow flag for real-time log streaming
- Supports --debuglogs flag to show hidden debug commands
- Uses API pagination (skip/take) for efficient deployment fetching
- Sorts deployments by timestamp to ensure latest is selected

Implementation:
- Add DeploymentsListResponse to handle paginated API responses
- Add log parsing utilities to format JSON logs as plain text
- Add comprehensive test coverage for deployment service methods
- Update README with new command documentation
2025-11-10 13:18:15 +01:00
Andras Bacsai 7c42af6203 Merge pull request #36 from coollabsio/fix/remove-file-check
Remove gzip file validation check
2025-11-10 12:34:29 +01:00
Andras Bacsai ed2dbd4947 Merge pull request #34 from YaRissi/version-inject
feat: Version inject
2025-11-10 12:33:57 +01:00
Laurence Jones f1301c1dbd Remove gzip file validation check
fix #35 

Removed basic check for gzip compressed file, since tar will error if not valid type anyways.
2025-11-07 08:08:31 +00:00
YaRissi 8a58cdd72f fix: update documentation to reflect new version retrieval location 2025-10-29 22:07:58 +01:00
Yassir Elmarissi 5ca4c3a8e3 Merge branch 'v4.x' into version-inject 2025-10-29 15:33:27 +01:00
Andras Bacsai 89cd744696 Merge pull request #33 from YaRissi/ci
feat: testing ci with linter
2025-10-29 15:22:39 +01:00
Andras Bacsai 694b4f8e32 Merge pull request #31 from YaRissi/go-install
feat: installable via go
2025-10-29 15:18:10 +01:00
Yassir Elmarissi ef15d013da fix: update regex for version injection
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-29 00:14:51 +01:00
YaRissi 9f5a44be04 fix: comment 2025-10-28 23:51:16 +01:00
YaRissi 1078a9d3ca fix: improve instance validation and error handling in context switching 2025-10-28 23:42:04 +01:00
YaRissi 9cef9ebee7 fix: improve error handling in commands 2025-10-28 23:27:03 +01:00
YaRissi 51f33cfc5e reactor: change context.Background zu cmd.Context 2025-10-28 23:15:02 +01:00
YaRissi 559d4e2709 chore: update commit message for version bump in release workflow 2025-10-21 01:02:43 +02:00
YaRissi 94e237e2b1 fix: specify branch reference in action and fix readme 2025-10-21 00:57:52 +02:00
YaRissi 3dd0dfdce1 fix: stage version file before committing 2025-10-21 00:34:10 +02:00
YaRissi aac44a0ddb fix: remove parallelism setting in goreleaser configuration 2025-10-21 00:28:22 +02:00
YaRissi a4c96d2803 fix: correct regex for version update in checker.go 2025-10-21 00:23:43 +02:00
YaRissi dc9670992a feat: implement version update automation 2025-10-21 00:13:55 +02:00
YaRissi c369689131 add coverage to test command 2025-10-18 13:50:45 +02:00
YaRissi dcf0b39c1b fix: golangci-lint version 2025-10-18 13:46:46 +02:00
YaRissi 14a3f00c57 refactor: update command handlers to use RunE for error handling 2025-10-18 13:37:31 +02:00
YaRissi 86f77716ee fix: all found problems by the linter 2025-10-18 13:13:35 +02:00
YaRissi ef91ed987e feat: testing ci with linter and pull request template 2025-10-18 13:12:21 +02:00
Yassir Elmarissi d8cf7a5986 docs: update installation section to emphasize the recommended install script 2025-10-17 21:32:44 +00:00
YaRissi d30b0b90de fix: run commands paths 2025-10-17 23:08:13 +02:00
YaRissi 4a8a659090 fix: update build commands and paths in configuration and documentation 2025-10-17 22:57:05 +02:00
Andras Bacsai aa1bda4063 Merge pull request #32 from coollabsio/andrasbacsai/env-sync-analysis
Add flags to env sync command
2025-10-17 22:29:43 +02:00
Andras Bacsai 69f2a7ac1f Changes auto-committed by Conductor 2025-10-17 22:29:14 +02:00
Yassir Elmarissi ad185a42ee update goreleaser 2025-10-17 14:58:05 +00:00
Yassir Elmarissi 22e34fb72e add dist to gitignore 2025-10-17 11:32:39 +00:00
Yassir Elmarissi c5b0ad4218 feat: installable via go and update gorealeser 2025-10-17 11:29:04 +00:00
Andras Bacsai 1a6fa9e397 Merge pull request #29 from coollabsio/andrasbacsai/parallel-goreleaser-builds
Enable parallel GoReleaser builds
2025-10-17 11:41:30 +02:00
Andras Bacsai b767468b29 Changes auto-committed by Conductor 2025-10-17 11:40:59 +02:00
Andras Bacsai 6b742d37dd Update checker.go 2025-10-17 11:35:47 +02:00
Andras Bacsai 0b2d277e41 Merge pull request #27 from coollabsio/contributing-guide
Add CONTRIBUTING.md file
2025-10-17 11:35:23 +02:00
Andras Bacsai ac9a486d46 Merge pull request #28 from coollabsio/andrasbacsai/add-min-version-reqs
Add minimum version checks to CLI commands
2025-10-17 11:35:15 +02:00
Andras Bacsai ba8f05769f Changes auto-committed by Conductor 2025-10-17 11:34:49 +02:00
Andras Bacsai 5c799410e5 Changes auto-committed by Conductor 2025-10-17 11:33:48 +02:00
Andras Bacsai d5dd1b5bdf Changes auto-committed by Conductor 2025-10-17 11:32:53 +02:00
Andras Bacsai 0d6a2bb1e9 Changes auto-committed by Conductor 2025-10-17 11:32:53 +02:00
Andras Bacsai 9eba5b97f7 Merge pull request #26 from coollabsio/andrasbacsai/check-readme
Update README with CLI command fixes
2025-10-17 11:26:10 +02:00
Andras Bacsai 76c1434711 Changes auto-committed by Conductor 2025-10-17 11:24:00 +02:00
Andras Bacsai f35d299f6e Update README.md 2025-10-17 11:17:17 +02:00
Andras Bacsai 680264ab3f Update README.md 2025-10-17 11:15:38 +02:00
Andras Bacsai 794025aee1 Changes auto-committed by Conductor (#25) 2025-10-17 11:14:24 +02:00
Andras Bacsai 8d6da93aa1 Merge pull request #24 from coollabsio/andrasbacsai/show-config-path
Add coolify config command
2025-10-17 11:05:57 +02:00
Andras Bacsai 22516fe51e Changes auto-committed by Conductor 2025-10-17 11:00:09 +02:00
Andras Bacsai fe63c3a3b6 Changes auto-committed by Conductor 2025-10-17 10:59:12 +02:00
178 changed files with 12296 additions and 1040 deletions
+2 -2
View File
@@ -5,14 +5,14 @@ tmp_dir = "tmp"
[build]
args_bin = []
bin = "./coolify"
cmd = "go build -o ./coolify ."
cmd = "go build -o ./coolify ./coolify"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", ".git", ".conductor"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = "echo 'Build complete. Binary: ./coolify'"
full_bin = "echo 'Build complete. Binary: ./coolify/coolify'"
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
+7
View File
@@ -0,0 +1,7 @@
## Changes
-
## Issues & Discussions
- fix #
+35 -5
View File
@@ -10,18 +10,48 @@ jobs:
release-cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: ${{ env.GITHUB_REF_NAME }}
args: release --clean
workdir: ./
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
update-version:
needs: [release-cli]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: v4.x
fetch-depth: 0
- name: Update version file
run: |
TAG=${GITHUB_REF#refs/tags/}
echo "Updating version to $TAG"
sed -i "s/^\tversion = \".*\"/\tversion = \"$TAG\"/" internal/version/checker.go
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
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
+75
View File
@@ -0,0 +1,75 @@
name: Testing CLI
on:
push:
branches: ["v4.x"]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Run gofmt
run: diff -u <(echo -n) <(gofmt -d -s .)
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.5.0 # pin version for consistency
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Run tests
run: go test -v -race -cover ./...
llms-txt:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Regenerate llms.txt
run: go run ./coolify docs llms
- name: Check uncommitted changes
run: git diff --exit-code llms.txt llms-full.txt
- if: failure()
run: echo "::error::llms.txt or llms-full.txt is out of date. Run 'go run ./coolify docs llms' and commit the changes."
go-mod-tidy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Run go mod tidy
run: go mod tidy
- name: Check uncommitted changes
run: git diff --exit-code
- if: failure()
run: echo "::error::Check failed, please run 'go mod tidy' and commit the changes."
+2 -1
View File
@@ -1,11 +1,12 @@
coolify-cli
coolify
/coolify
config.json
.claude
# Generated documentation (can be regenerated)
man/
docs/cli/
dist/
# Test coverage
coverage.out
+75
View File
@@ -0,0 +1,75 @@
version: "2"
run:
timeout: 5m
linters:
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- contextcheck
- durationcheck
- errchkjson
- errorlint
- exhaustive
- gocheckcompilerdirectives
- gochecksumtype
- gocritic
- gomoddirectives
- gomodguard
- gosec
- gosmopolitan
- loggercheck
- makezero
- musttag
- nilerr
- nilnesserr
- noctx
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck
- spancheck
- sqlclosecheck
- testifylint
- unparam
- zerologlint
settings:
exhaustive:
default-signifies-exhaustive: true
staticcheck:
checks: ["all", "-ST1005", "-S1016"]
gosec:
excludes:
- G115
gosmopolitan:
allow-time-local: true
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- std-error-handling
formatters:
enable:
- gci
- goimports
settings:
gci:
sections:
- standard
- default
- prefix(github.com/coollabsio)
exclusions:
generated: lax
+42 -2
View File
@@ -1,8 +1,19 @@
version: 2
before:
hooks:
- go mod tidy
builds:
- binary: coolify
- id: coolify
binary: coolify
flags:
- -trimpath
ldflags:
- -s
- -w
- -X github.com/coollabsio/coolify-cli/internal/version.version={{ .Version }}
main: ./coolify/main.go
goos:
- darwin
- linux
@@ -11,4 +22,33 @@ 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]
brews:
- name: coolify-cli
repository:
owner: coollabsio
name: homebrew-coolify-cli
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
directory: Formula
homepage: "https://coolify.io"
description: "CLI tool for interacting with the Coolify API"
license: "MIT"
install: |
bin.install "coolify"
test: |
system "#{bin}/coolify", "version"
+4 -1
View File
@@ -550,7 +550,10 @@ c.httpClient = &http.Client{
```bash
# Local build
go build -o coolify .
go build -o coolify ./coolify
# Install locally
go install ./coolify
# Multi-platform release
goreleaser release --clean
+7 -6
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`)
@@ -21,7 +22,7 @@ All commands in this CLI are wrappers around API endpoints defined in the OpenAP
### Command Structure
The codebase follows Cobra's command pattern with a root command and subcommands:
- Entry point: `main.go` calls `cmd.Execute()`
- Entry point: `coolify/main.go` calls `cmd.Execute()`
- Root command: `cmd/root.go` - contains core utilities (HTTP client, authentication, version checking, config management)
- Subcommands: Each command is in its own file in `cmd/`:
- `context.go` - manage Coolify context (add, remove, list, set default/token)
@@ -62,23 +63,23 @@ Three output modes supported via `--format` flag:
### Build
```bash
go build -o coolify .
go build -o coolify ./coolify
```
### Run locally
```bash
go run main.go [command]
go run ./coolify [command]
```
### Test a command
```bash
go run main.go instances list
go run main.go servers list --debug
go run ./coolify context list
go run ./coolify servers list --debug
```
### Install locally
```bash
go install
go install ./coolify
```
### Run tests
+620
View File
@@ -0,0 +1,620 @@
# Contributing to Coolify CLI
Thank you for your interest in contributing to the Coolify CLI! This document provides guidelines and instructions for contributing to the project.
## Table of Contents
- [Getting Started](#getting-started)
- [Development Setup](#development-setup)
- [Project Architecture](#project-architecture)
- [Adding a New Command](#adding-a-new-command)
- [Testing Requirements](#testing-requirements)
- [Code Style & Conventions](#code-style--conventions)
- [Submitting Changes](#submitting-changes)
## Getting Started
Before you start contributing:
1. **Read the [ARCHITECTURE.md](ARCHITECTURE.md)** for detailed architectural guidance
2. **Review the [OpenAPI specification](https://github.com/coollabsio/coolify/blob/v4.x/openapi.json)** to understand available API endpoints
3. **Check existing issues** to see if your feature/bug is already being worked on
4. **Open an issue** to discuss your proposed changes (for large features)
### Prerequisites
- Go 1.24 or higher
- Git
## Development Setup
### Clone and Build
```bash
# Fork the repository on GitHub
# Clone your fork
git clone https://github.com/YOUR_USERNAME/coolify-cli.git
cd coolify-cli
# Build the CLI
go build -o coolify ./coolify
# Install locally
go install
```
### Running the CLI
```bash
# Run without installing
go run ./coolify [command]
# Example commands
go run ./coolify context list
go run ./coolify server list --debug
# With flags
go run ./coolify server list --format json --debug
```
### Project Structure
```
cmd/ # CLI commands (organized by feature)
├── root.go # Root command and global flags
├── application/ # Application management commands
├── context/ # Manage Coolify instances
├── server/ # Server management
├── project/ # Project management
├── database/ # Database management
├── deployment/ # Deployment operations
├── service/ # Service management
└── ...
internal/ # Internal packages
├── api/ # API client (HTTP communication)
├── cli/ # CLI utilities (GetAPIClient helper)
├── config/ # Configuration management
├── models/ # Data models and structs
├── output/ # Output formatters (table, json, pretty)
├── parser/ # Input parsing utilities
├── service/ # Business logic layer
└── version/ # Version management
test/ # Test utilities and fixtures
└── fixtures/ # Mock API response data
```
## Project Architecture
The Coolify CLI follows a **layered architecture**:
```
User → Commands (cmd/) → Services (internal/service/) → API Client (internal/api/) → Coolify API
```
### Layer Responsibilities
1. **Command Layer** (`cmd/`)
- Parse CLI arguments and flags
- Call service layer methods
- Format output using output formatters
2. **Service Layer** (`internal/service/`)
- Business logic
- Coordinate API calls
- Transform data
3. **API Client Layer** (`internal/api/`)
- HTTP communication
- Retry logic with exponential backoff
- Authentication (Bearer tokens)
- Error handling
### Key Dependencies
- **cobra**: CLI framework
- **viper**: Configuration management
- **stretchr/testify**: Testing assertions
## Adding a New Command
Follow these steps to add a new command:
### 1. Create Command Directory Structure
```bash
# Create directory for your command
mkdir -p cmd/myfeature
```
### 2. Create Parent Command
Create `cmd/myfeature/myfeature.go`:
```go
package myfeature
import "github.com/spf13/cobra"
// NewMyFeatureCommand creates the myfeature parent command
func NewMyFeatureCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "myfeature",
Aliases: []string{"mf"},
Short: "MyFeature related commands",
Long: `Manage MyFeature resources.`,
}
// Add subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
// ... more subcommands
return cmd
}
```
### 3. Create Subcommand
Create `cmd/myfeature/list.go`:
```go
package myfeature
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all myfeature resources",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Get API client
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Use service layer
svc := service.NewMyFeatureService(client)
items, err := svc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list items: %w", err)
}
// Format output
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(items)
},
}
}
```
### 4. Create Service Layer
Create `internal/service/myfeature.go`:
```go
package service
import (
"context"
"github.com/coollabsio/coolify-cli/internal/api"
"github.com/coollabsio/coolify-cli/internal/models"
)
type MyFeatureService struct {
client *api.Client
}
func NewMyFeatureService(client *api.Client) *MyFeatureService {
return &MyFeatureService{client: client}
}
func (s *MyFeatureService) List(ctx context.Context) ([]models.MyFeature, error) {
var items []models.MyFeature
err := s.client.Get(ctx, "myfeature", &items)
return items, err
}
func (s *MyFeatureService) Get(ctx context.Context, uuid string) (*models.MyFeature, error) {
var item models.MyFeature
err := s.client.Get(ctx, "myfeature/"+uuid, &item)
return &item, err
}
func (s *MyFeatureService) Create(ctx context.Context, req models.MyFeatureCreateRequest) (*models.Response, error) {
var response models.Response
err := s.client.Post(ctx, "myfeature", req, &response)
return &response, err
}
func (s *MyFeatureService) Delete(ctx context.Context, uuid string) error {
return s.client.Delete(ctx, "myfeature/"+uuid)
}
```
### 5. Create Models
Create `internal/models/myfeature.go`:
```go
package models
type MyFeature struct {
ID int `json:"id" table:"-"` // Hidden from table output
UUID string `json:"uuid"` // Shown to users
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status"`
// Add more fields...
}
type MyFeatureCreateRequest struct {
Name string `json:"name"`
Description string `json:"description"`
}
```
**Important**: Always use `UUID` for user-facing identifiers, not database `ID`. Hide `ID` field from table output using `table:"-"` tag.
### 6. Register Command
Add your command to `cmd/root.go`:
```go
import (
// ... existing imports
"github.com/coollabsio/coolify-cli/cmd/myfeature"
)
func init() {
// ... existing code
rootCmd.AddCommand(myfeature.NewMyFeatureCommand())
}
```
### 7. Create Tests
Create `internal/service/myfeature_test.go`:
```go
package service
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/coollabsio/coolify-cli/internal/api"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMyFeatureService_List(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/myfeature", r.URL.Path)
assert.Equal(t, "GET", r.Method)
items := []models.MyFeature{
{UUID: "uuid-1", Name: "item-1"},
{UUID: "uuid-2", Name: "item-2"},
}
json.NewEncoder(w).Encode(items)
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
svc := NewMyFeatureService(client)
items, err := svc.List(cmd.Context())
require.NoError(t, err)
assert.Len(t, items, 2)
assert.Equal(t, "uuid-1", items[0].UUID)
}
```
### 8. Update Documentation
- Add command documentation to `README.md`
- Include usage examples and flag descriptions
## Testing Requirements
**All code changes MUST include tests.** This is non-negotiable.
### Coverage Requirements
- **Minimum coverage**: 70% for all packages
- **New features**: 80%+ coverage required
- **Bug fixes**: Must include regression tests
- **Refactoring**: Must maintain or improve existing coverage
### Running Tests
```bash
# Run all tests
go test ./internal/...
# Run with coverage
go test ./internal/... -cover
# Run specific package
go test ./internal/service/... -v
# Run specific test
go test ./internal/service -run TestServerService_List -v
# Generate coverage report
go test ./internal/... -coverprofile=coverage.out
go tool cover -html=coverage.out
```
### Writing Tests
#### Use Table-Driven Tests
```go
func TestMyFunction(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "successful case",
input: "test",
want: "expected",
wantErr: false,
},
{
name: "error case",
input: "",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := MyFunction(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("MyFunction() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("MyFunction() = %v, want %v", got, tt.want)
}
})
}
}
```
#### Mock HTTP Requests
**IMPORTANT**: Never call real APIs in tests. Use `httptest.NewServer()`:
```go
func TestServiceMethod(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
assert.Equal(t, "/api/v1/endpoint", r.URL.Path)
assert.Equal(t, "GET", r.Method)
// Return mock response
response := models.MyResponse{Data: "test"}
json.NewEncoder(w).Encode(response)
}))
defer server.Close()
client := api.NewClient(server.URL, "test-token")
// ... test your service
}
```
### Test Guidelines
- **Test naming**: `TestFunctionName_Scenario_ExpectedBehavior`
- **Use subtests**: `t.Run()` for related test cases
- **Use testify**: `require.NoError()` for must-pass assertions, `assert.Equal()` for comparisons
- **Mock HTTP**: Use `httptest.NewServer()` for all API tests
- **Test contexts**: Always pass `context.Background()` in tests
- **Test errors**: Verify error messages and types
## Code Style & Conventions
### Go Standards
- Follow standard Go idioms and conventions
- Use `gofmt` for code formatting
- Run `go vet` to catch common issues
- Prefer standard library over external dependencies
### Project Conventions
#### API Client Usage
```go
// Create client (usually done via cli.GetAPIClient())
client := api.NewClient(baseURL, token, api.WithDebug(true))
// GET request
var result MyStruct
err := client.Get(ctx, "endpoint", &result)
// POST request
err := client.Post(ctx, "endpoint", requestBody, &result)
// DELETE request
err := client.Delete(ctx, "endpoint")
// PATCH request
err := client.Patch(ctx, "endpoint", requestBody, &result)
```
#### Service Layer Pattern
```go
type MyService struct {
client *api.Client
}
func NewMyService(client *api.Client) *MyService {
return &MyService{client: client}
}
func (s *MyService) List(ctx context.Context) ([]models.Item, error) {
var items []models.Item
err := s.client.Get(ctx, "items", &items)
return items, err
}
```
#### Error Handling
```go
// Wrap errors with context
if err != nil {
return fmt.Errorf("failed to fetch data: %w", err)
}
// Check and handle specific error types
if apiErr, ok := err.(*api.Error); ok {
if apiErr.StatusCode == 404 {
return fmt.Errorf("resource not found")
}
}
```
#### Global Flags
All commands automatically inherit these global flags:
- `--format` (table|json|pretty) - Output format
- `--show-sensitive` - Show sensitive information
- `--debug` - Enable debug mode
- `--context` - Use specific context by name
- `--token` - Override context token
Access flags in commands:
```go
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
debug, _ := cmd.Flags().GetBool("debug")
```
## Submitting Changes
### Before Committing
```bash
# 1. Format code
go fmt ./...
# 2. Run tests
go test ./internal/...
# 3. Check coverage
go test ./internal/... -cover
# 4. Run vet
go vet ./...
```
### Commit Messages
Write clear, descriptive commit messages following conventional commits format:
```
<type>: <short summary>
<detailed description>
<footer>
```
Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`
Example:
```
feat: add server domains list command
- Implement GET /servers/{uuid}/domains endpoint
- Add server domains subcommand
- Include tests for domain listing
- Update README with new command documentation
```
### Pull Requests
1. **Fork** the repository
2. **Create a branch** from `v4.x`: `git checkout -b feature/my-feature v4.x`
3. **Make your changes** with tests
4. **Push** to your fork: `git push origin feature/my-feature`
5. **Open a pull request** against the `v4.x` branch
6. **Describe your changes** clearly in the PR description
7. **Link related issues** using "Fixes #123" or "Closes #123"
### PR Checklist
- [ ] Tests pass locally (`go test ./internal/...`)
- [ ] Code coverage meets requirements (70%+ minimum)
- [ ] Code is formatted (`go fmt ./...`)
- [ ] README.md updated (if adding new commands)
- [ ] CLAUDE.md updated (if changing architecture)
- [ ] Commit messages are descriptive
- [ ] PR description explains the changes
- [ ] All global flags are supported (format, show-sensitive, debug)
- [ ] Used UUIDs (not IDs) for resource identifiers
## Release Process (not for contributors :) )
Releases are automated using GoReleaser:
1. Tag a new version: `git tag v1.2.3`
2. Push the tag: `git push origin v1.2.3`
3. Create a GitHub release
4. GoReleaser builds binaries for all platforms automatically
## Getting Help
- **Discord**: https://coolify.io/discord
- **Issues**: [Open an issue](https://github.com/coollabsio/coolify-cli/issues) for bugs or feature requests
- **Architecture**: Read [ARCHITECTURE.md](ARCHITECTURE.md) for detailed design documentation
- **API Reference**: See the [OpenAPI specification](https://github.com/coollabsio/coolify/blob/v4.x/openapi.json)
- **Code Guidance**: See [CLAUDE.md](CLAUDE.md) for AI assistant guidance
## License
By contributing, you agree that your contributions will be licensed under the same license as the project.
---
Thank you for contributing to Coolify CLI! 🚀
+10 -28
View File
@@ -10,28 +10,7 @@ This guide explains the release process for the Coolify CLI.
## Release Process
### 1. Update Version Number
Edit `cmd/root.go` and update the `CliVersion` variable:
```go
var CliVersion = "1.x.x" // Change to your new version
```
**Version Format:** Use semantic versioning: `MAJOR.MINOR.PATCH` (e.g., `1.2.3`)
- **MAJOR**: Breaking changes
- **MINOR**: New features (backwards compatible)
- **PATCH**: Bug fixes (backwards compatible)
### 2. Commit and Push Version Change
```bash
git add cmd/root.go
git commit -m "chore: bump version to 1.x.x"
git push origin v4.x
```
### 3. Create a GitHub Release
### 1. Create a GitHub Release
1. Go to https://github.com/coollabsio/coolify-cli/releases/new
2. Click "Choose a tag" and create a new tag:
@@ -56,7 +35,7 @@ git push origin v4.x
```
5. Click "Publish release"
### 4. Automated Build Process
### 2. Automated Build Process
Once you publish the release:
@@ -65,12 +44,14 @@ Once you publish the release:
- **Linux**: amd64, arm64
- **macOS (Darwin)**: amd64, arm64
- **Windows**: amd64, arm64
3. Binaries are automatically uploaded to the release
4. The release becomes available at:
3. Goreleaser injects the version from the tag into the binaries
4. Binaries are automatically uploaded to the release
5. The release becomes available at:
- GitHub: `https://github.com/coollabsio/coolify-cli/releases/tag/v1.x.x`
- Install script: `curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash`
- `go install`: `go install github.com/coollabsio/coolify-cli/coolify@v1.x.x`
### 5. Verify the Release
### 3. Verify the Release
After the workflow completes (usually 2-5 minutes):
@@ -128,10 +109,11 @@ After creating a release:
The release process uses these configuration files:
- `.goreleaser.yml` - GoReleaser configuration (build matrix, archives, etc.)
- `.goreleaser.yml` - GoReleaser configuration (build matrix, archives, etc.) - points to `/coolify` as entry point
- `.github/workflows/release-cli.yml` - GitHub Actions workflow
- `scripts/install.sh` - User-facing install script
- `cmd/root.go` - Contains `CliVersion` variable (line 22)
- `internal/version/checker.go` - Contains `GetVersion()` function that returns the current version
- `coolify/main.go` - Binary entry point for `go install` support
## Notes
+125 -38
View File
@@ -2,13 +2,50 @@
## Installation
### 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`
> If you are a windows or mac user, please test the installation script and let us know if it works for you.
### Homebrew (macOS/Linux)
```bash
brew install coollabsio/coolify-cli/coolify-cli
```
#### 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
go install github.com/coollabsio/coolify-cli/coolify@latest
```
This will install the `coolify` binary in your `$GOPATH/bin` directory (usually `~/go/bin`). Make sure this directory is in your `$PATH`.
### Using the install script
## Getting Started
1. Get a `<token>` from your Coolify dashboard (Cloud or self-hosted) at `/security/api-tokens`
@@ -27,6 +64,16 @@ It will install the CLI in `/usr/local/bin/coolify` and the configuration file i
Now you can use the CLI with the token you just added.
## For LLMs / AI agents
- Quick instructions: [`llms.txt`](./llms.txt)
- Full command catalog: [`llms-full.txt`](./llms-full.txt)
- Regenerate both files:
```bash
go run ./coolify docs llms
```
## Change default context
You can change the default context with `coolify context use <context_name>` or `coolify context set-default <context_name>`
## Currently Supported Commands
@@ -34,6 +81,13 @@ You can change the default context with `coolify context use <context_name>` or
### Update
- `coolify update` - Update the CLI to the latest version
### Configuration
- `coolify config` - Show configuration file location
### Shell Completion
- `coolify completion <shell>` - Generate shell completion script
- Supported shells: `bash`, `zsh`, `fish`, `powershell`
### Context Management
- `coolify context list` - List all configured contexts
- `coolify context add <context_name> <url> <token>` - Add a new context
@@ -48,25 +102,23 @@ You can change the default context with `coolify context use <context_name>` or
- `--url <new_url>` - Change the context URL
- `--token <new_token>` - Change the context token
- `coolify context use <context_name>` - Switch to a different context (set as default)
- `coolify context verify` - Verify current context connection and authentication
- `coolify context version` - Get the Coolify API version of the current context
### Servers
- `coolify servers list` - List all servers
- `coolify servers get <uuid>` - Get a server by UUID
Commands can use `server` or `servers` interchangeably.
- `coolify server list` - List all servers
- `coolify server get <uuid>` - Get a server by UUID
- `--resources` - Get the resources and their status of a server
<<<<<<< HEAD
- `coolify servers add <server_name> <ip_address> <private_key_uuid>` - Add a new server
- `--port <port>` - SSH port (default: 22)
- `--user <user>` - SSH user (default: root)
=======
- `coolify servers add <name> <ip> <private_key_uuid>` - Add a new server
- `coolify server add <name> <ip> <private_key_uuid>` - Add a new server
- `-p, --port <port>` - SSH port (default: 22)
- `-u, --user <user>` - SSH user (default: root)
>>>>>>> origin/v4.x
- `--validate` - Validate server immediately after adding
- `coolify servers remove <uuid>` - Remove a server
- `coolify servers validate <uuid>` - Validate a server connection
- `coolify servers domains <uuid>` - Get server domains by UUID
- `coolify server remove <uuid>` - Remove a server
- `coolify server validate <uuid>` - Validate a server connection
- `coolify server domains <uuid>` - Get server domains by UUID
### Projects
- `coolify projects list` - List all projects
@@ -113,10 +165,30 @@ You can change the default context with `coolify context use <context_name>` or
- `--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> <env_uuid_or_key>` - Update an environment variable
- `--value <value>` - Variable value (required)
- `--key <key>` - New variable key (optional, for renaming)
- `--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)
- `--build-time` - Make all variables available at build time
- `--preview` - Make all variables available in preview deployments
- `--is-literal` - Treat all values as literal (don't interpolate variables)
- **Behavior**: Updates existing variables, creates missing ones. Does NOT delete variables not in the file.
#### Application Deployments
- `coolify app deployments list <app-uuid>` - List all deployments for an application
- `coolify app deployments logs <app-uuid> [deployment-uuid]` - Get deployment logs (formatted as human-readable text)
- If only `app-uuid` is provided: retrieves logs from the **latest/most recent deployment only**
- If `deployment-uuid` is also provided: retrieves logs for that **specific deployment**
- `-n, --lines <n>` - Number of log lines to display (default: 0 = all lines)
- `-f, --follow` - Follow log output in real-time (like tail -f)
- `--debuglogs` - Show debug logs (includes hidden commands and internal operations)
### Databases
- `coolify database list` - List all databases
@@ -183,24 +255,34 @@ You can change the default context with `coolify context use <context_name>` or
- `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> <env_uuid_or_key>` - Update an environment variable
- `--value <value>` - Variable value (required)
- `--key <key>` - New variable key (optional, for renaming)
- `--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)
- `--build-time` - Make all variables available at build time
- `--preview` - Make all variables available in preview deployments
- `--is-literal` - Treat all values as literal (don't interpolate variables)
- **Behavior**: Updates existing variables, creates missing ones. Does NOT delete variables not in the file.
### Deployments
- `coolify deploy uuid <uuid>` - Deploy a resource by UUID
<<<<<<< HEAD
- `--force` - Force deployment
- `coolify deploy name <resource_name>` - Deploy a resource by name
- `--force` - Force deployment
=======
- `-f, --force` - Force deployment
- `--pull-request-id <id>` - Pull request ID for preview deployments
- `--docker-tag <tag>` - Docker image tag override for the deployment (requires Coolify `4.0.0-beta.471+`)
- `coolify deploy name <name>` - Deploy a resource by name
- `-f, --force` - Force deployment
>>>>>>> origin/v4.x
- `--pull-request-id <id>` - Pull request ID for preview deployments
- `--docker-tag <tag>` - Docker image tag override for the deployment (requires Coolify `4.0.0-beta.471+`)
- `coolify deploy batch <name1,name2,...>` - Deploy multiple resources at once
- `-f, --force` - Force all deployments
- `--pull-request-id <id>` - Pull request ID for preview deployments
- `--docker-tag <tag>` - Docker image tag override for the deployment (requires Coolify `4.0.0-beta.471+`)
- `coolify deploy list` - List all deployments
- `coolify deploy get <uuid>` - Get deployment details
- `coolify deploy cancel <uuid>` - Cancel a deployment
@@ -236,20 +318,19 @@ You can change the default context with `coolify context use <context_name>` or
- `coolify team members list [team_id]` - List team members
### Private Keys
- `coolify privatekeys list` - List all private keys
- `coolify privatekeys create <key_name> <private-key>` - Create a new private key
- Use `@filename` to read from file: `coolify privatekeys create mykey @~/.ssh/id_rsa`
- `coolify privatekeys delete <uuid>` - Delete a private key
Commands can use `private-key`, `private-keys`, `key`, or `keys` interchangeably.
- `coolify private-key list` - List all private keys
- `coolify private-key add <key_name> <private-key>` - Add a new private key
- Use `@filename` to read from file: `coolify private-key add mykey @~/.ssh/id_rsa`
- `coolify private-key remove <uuid>` - Remove a private key
## Global Flags
All commands support these global flags:
<<<<<<< HEAD
- `--context <name>` - Use a specific context instead of default
=======
- `--instance <name>` - Use a specific instance profile instead of default
>>>>>>> origin/v4.x
- `--host <fqdn>` - Override the Coolify instance hostname
- `--token <token>` - Override the authentication token
- `--format <format>` - Output format: `table` (default), `json`, or `pretty`
@@ -299,7 +380,10 @@ coolify app logs <uuid>
# Environment variables
coolify app env list <uuid>
coolify app env create <uuid> --key API_KEY --value secret123
# Sync from .env file (updates existing, creates new, keeps others unchanged)
coolify app env sync <uuid> --file .env
coolify app env sync <uuid> --file .env.production --build-time --preview
```
### Database Management
@@ -359,6 +443,9 @@ coolify deploy batch api,worker,frontend
# Force deploy with specific context
coolify --context=prod deploy batch api,worker --force
# Deploy a preview with an explicit docker tag
coolify deploy uuid u5ualfp30j27qtfpgcen8p03 --pull-request-id 2345 --docker-tag 1.28.3
# Traditional UUID deployment still works
coolify deploy uuid abc123-def456-...
@@ -414,13 +501,13 @@ coolify team members list
```bash
# List servers in production
coolify --context=prod servers list
coolify --context=prod server list
# Add a server with validation
coolify servers add myserver 192.168.1.100 <key-uuid> --validate
coolify server add myserver 192.168.1.100 <key-uuid> --validate
# Get server details with resources
coolify servers get <uuid> --resources
coolify server get <uuid> --resources
```
## Output Formats
@@ -429,13 +516,13 @@ The CLI supports three output formats:
```bash
# Table format (default, human-readable)
coolify servers list
coolify server list
# JSON format (for scripts)
coolify servers list --format=json
coolify server list --format=json
# Pretty JSON (for debugging)
coolify servers list --format=pretty
coolify server list --format=pretty
```
## Architecture
@@ -450,7 +537,7 @@ This CLI follows a clean architecture with:
```bash
# Build
go build -o coolify .
go build -o coolify ./coolify
# Run tests
go test ./...
@@ -459,12 +546,12 @@ go test ./...
go test -cover ./...
# Install locally
go install
go install ./coolify
```
## Contributing
Contributions are welcome! Please check the [restructure documentation](RESTRUCTURE_PLAN.md) for architecture guidelines.
Contributions are welcome!
## License
+17
View File
@@ -3,7 +3,9 @@ package application
import (
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/cmd/application/create"
"github.com/coollabsio/coolify-cli/cmd/application/env"
"github.com/coollabsio/coolify-cli/cmd/application/storage"
)
// NewAppCommand creates the app parent command
@@ -18,12 +20,14 @@ 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())
cmd.AddCommand(NewStopCommand())
cmd.AddCommand(NewRestartCommand())
cmd.AddCommand(NewLogsCommand())
cmd.AddCommand(NewDeploymentsCommand())
// Add env subcommand with its children
envCmd := &cobra.Command{
@@ -40,5 +44,18 @@ func NewAppCommand() *cobra.Command {
envCmd.AddCommand(env.NewSyncEnvCommand())
cmd.AddCommand(envCmd)
// Add storage subcommand with its children
storageCmd := &cobra.Command{
Use: "storage",
Aliases: []string{"storages"},
Short: "Manage application storages",
Long: `List and manage persistent volumes and file storages for applications.`,
}
storageCmd.AddCommand(storage.NewListCommand())
storageCmd.AddCommand(storage.NewCreateCommand())
storageCmd.AddCommand(storage.NewUpdateCommand())
storageCmd.AddCommand(storage.NewDeleteCommand())
cmd.AddCommand(storageCmd)
return cmd
}
+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
}
+154
View File
@@ -0,0 +1,154 @@
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)
setOptionalStringFlag(cmd, "dockerfile-target-build", &req.DockerfileTargetBuild)
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")
cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage")
return cmd
}
+122
View File
@@ -0,0 +1,122 @@
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)
setOptionalStringFlag(cmd, "dockerfile-target-build", &req.DockerfileTargetBuild)
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")
cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage")
return cmd
}
+129
View File
@@ -0,0 +1,129 @@
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)
setOptionalStringFlag(cmd, "dockerfile-target-build", &req.DockerfileTargetBuild)
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")
cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage")
return cmd
}
+155
View File
@@ -0,0 +1,155 @@
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)
setOptionalStringFlag(cmd, "dockerfile-target-build", &req.DockerfileTargetBuild)
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")
cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage")
return cmd
}
+160
View File
@@ -0,0 +1,160 @@
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)
setOptionalStringFlag(cmd, "dockerfile-target-build", &req.DockerfileTargetBuild)
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")
cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage")
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
}
}
+8 -4
View File
@@ -1,12 +1,12 @@
package application
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewDeleteCommand() *cobra.Command {
@@ -16,7 +16,7 @@ func NewDeleteCommand() *cobra.Command {
Long: `Delete an application. This action cannot be undone.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
force, _ := cmd.Flags().GetBool("force")
@@ -24,7 +24,11 @@ func NewDeleteCommand() *cobra.Command {
if !force {
var response string
fmt.Printf("Are you sure you want to delete application %s? This cannot be undone. (yes/no): ", uuid)
fmt.Scanln(&response)
_, err := fmt.Scanln(&response)
if err != nil {
return fmt.Errorf("failed to read input: %w", err)
}
if response != "yes" && response != "y" {
fmt.Println("Delete cancelled.")
+182
View File
@@ -0,0 +1,182 @@
package application
import (
"context"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
)
func NewDeploymentsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "deployments",
Short: "Deployment related commands for an application",
Long: `Manage deployments for a specific application. List deployments or view deployment logs.`,
}
cmd.AddCommand(NewListDeploymentsCommand())
cmd.AddCommand(NewLogsDeploymentsCommand())
return cmd
}
func NewListDeploymentsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list <app-uuid>",
Short: "List all deployments for an application",
Long: `Retrieve a list of all deployments for a specific application.`,
Args: cli.ExactArgs(1, "<app-uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
deploySvc := service.NewDeploymentService(client)
deployments, err := deploySvc.ListByApplication(ctx, appUUID)
if err != nil {
return fmt.Errorf("failed to list deployments: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(deployments)
},
}
return cmd
}
func NewLogsDeploymentsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "logs <app-uuid> [deployment-uuid]",
Short: "Get deployment logs for an application",
Long: `Retrieve deployment logs for a specific application or deployment.
If only app-uuid is provided, retrieves logs from the latest deployment.
If deployment-uuid is also provided, retrieves logs for that specific deployment.
Use --follow to continuously stream new logs.`,
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
appUUID := args[0]
var deploymentUUID string
if len(args) == 2 {
deploymentUUID = args[1]
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
lines, _ := cmd.Flags().GetInt("lines")
follow, _ := cmd.Flags().GetBool("follow")
debugLogs, _ := cmd.Flags().GetBool("debuglogs")
format, _ := cmd.Flags().GetString("format")
deploySvc := service.NewDeploymentService(client)
// Function to get logs based on whether we have a deployment UUID
// Returns raw or formatted based on format flag
getLogs := func() (string, error) {
if deploymentUUID != "" {
return deploySvc.GetLogsByDeploymentWithFormat(ctx, deploymentUUID, debugLogs, format)
}
// Get logs from the latest deployment
// Use take=1 internally to efficiently fetch only the most recent deployment
return deploySvc.GetLogsByApplicationWithFormat(ctx, appUUID, 1, debugLogs, format)
}
if !follow {
logs, err := getLogs()
if err != nil {
return fmt.Errorf("failed to get deployment logs: %w", err)
}
// Apply line limit if specified (only for text output)
if lines > 0 && format == "table" {
logs = limitLogLines(logs, lines)
}
fmt.Print(logs)
return nil
}
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
lastLogs := ""
logs, err := getLogs()
if err != nil {
return fmt.Errorf("failed to get deployment logs: %w", err)
}
fmt.Print(logs)
lastLogs = logs
for {
select {
case <-sigChan:
fmt.Println("\nStopping log follow...")
return nil
case <-ticker.C:
logs, err := getLogs()
if err != nil {
continue
}
if logs != lastLogs {
if len(logs) > len(lastLogs) && strings.HasPrefix(logs, lastLogs) {
fmt.Print(logs[len(lastLogs):])
} else {
fmt.Print(logs)
}
lastLogs = logs
}
}
}
},
}
cmd.Flags().IntP("lines", "n", 0, "Number of log lines to display (0 = all)")
cmd.Flags().BoolP("follow", "f", false, "Follow log output (like tail -f)")
cmd.Flags().Bool("debuglogs", false, "Show debug logs (includes hidden commands and internal operations)")
return cmd
}
// limitLogLines limits the output to the last N lines
func limitLogLines(logs string, n int) string {
if n <= 0 {
return logs
}
// Trim trailing newline to avoid empty element at the end
logs = strings.TrimRight(logs, "\n")
lines := strings.Split(logs, "\n")
// If we have fewer lines than requested, return all
if len(lines) <= n {
return logs + "\n"
}
// Get the last N lines
lastLines := lines[len(lines)-n:]
return strings.Join(lastLines, "\n") + "\n"
}
+18 -8
View File
@@ -1,13 +1,13 @@
package env
import (
"context"
"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/service"
"github.com/spf13/cobra"
)
func NewCreateEnvCommand() *cobra.Command {
@@ -15,10 +15,10 @@ func NewCreateEnvCommand() *cobra.Command {
Use: "create <app_uuid>",
Short: "Create an environment variable for an application",
Long: `Create a new environment variable for a specific application. Use --key and --value flags to specify the variable.`,
Args: cli.ExactArgs(1, "<uuid>"),
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
uuid := args[0]
ctx := cmd.Context()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
@@ -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,14 +57,21 @@ func NewCreateEnvCommand() *cobra.Command {
if cmd.Flags().Changed("is-multiline") {
req.IsMultiline = &isMultiline
}
if cmd.Flags().Changed("runtime") {
req.IsRuntime = &isRuntime
}
if cmd.Flags().Changed("comment") {
comment, _ := cmd.Flags().GetString("comment")
req.Comment = &comment
}
appSvc := service.NewApplicationService(client)
env, err := appSvc.CreateEnv(ctx, uuid, req)
env, err := appSvc.CreateEnv(ctx, appUUID, req)
if err != nil {
return fmt.Errorf("failed to create environment variable: %w", err)
}
fmt.Printf("Environment variable '%s' created successfully.\n", env.Key)
fmt.Printf("Environment variable '%s' created successfully.\n", key)
fmt.Printf("UUID: %s\n", env.UUID)
return nil
},
@@ -71,9 +79,11 @@ 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)")
cmd.Flags().String("comment", "", "Comment for the environment variable")
return cmd
}
+8 -4
View File
@@ -1,12 +1,12 @@
package env
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewDeleteEnvCommand() *cobra.Command {
@@ -16,7 +16,7 @@ func NewDeleteEnvCommand() *cobra.Command {
Long: `Delete an environment variable from an application. First UUID is the application, second is the specific environment variable to delete.`,
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
appUUID := args[0]
envUUID := args[1]
@@ -31,7 +31,11 @@ func NewDeleteEnvCommand() *cobra.Command {
if !force {
var response string
fmt.Printf("Are you sure you want to delete this environment variable? (yes/no): ")
fmt.Scanln(&response)
_, err := fmt.Scanln(&response)
if err != nil {
return fmt.Errorf("failed to read confirmation: %w", err)
}
if response != "yes" && response != "y" {
fmt.Println("Delete cancelled.")
+11 -7
View File
@@ -1,25 +1,25 @@
package env
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewGetEnvCommand() *cobra.Command {
return &cobra.Command{
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.`,
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
Args: cli.ExactArgs(2, "<app_uuid> <env_uuid_or_key>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
appUUID := args[0]
envUUID := args[1]
envUUIDOrKey := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
@@ -27,7 +27,9 @@ func NewGetEnvCommand() *cobra.Command {
}
appSvc := service.NewApplicationService(client)
env, err := appSvc.GetEnv(ctx, appUUID, envUUID)
// 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
}
+35 -5
View File
@@ -1,23 +1,25 @@
package env
import (
"context"
"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"
"github.com/spf13/cobra"
)
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 := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
@@ -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
}
+9 -4
View File
@@ -1,15 +1,15 @@
package env
import (
"context"
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/parser"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewSyncEnvCommand() *cobra.Command {
@@ -24,7 +24,7 @@ func NewSyncEnvCommand() *cobra.Command {
Example: coolify app env sync abc123 --file .env.production`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
@@ -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
}
+40 -16
View File
@@ -1,39 +1,54 @@
package env
import (
"context"
"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/service"
"github.com/spf13/cobra"
)
func NewUpdateEnvCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <app_uuid> <env_uuid>",
Use: "update <app_uuid> <env_uuid_or_key>",
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. Identify it by UUID or key name.`,
Args: cli.ExactArgs(2, "<app_uuid> <env_uuid_or_key>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
appUUID := args[0]
envUUID := args[1]
envIdentifier := 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
}
appSvc := service.NewApplicationService(client)
// Look up the env var to resolve its key
existingEnv, err := appSvc.GetEnv(ctx, appUUID, envIdentifier)
if err != nil {
return fmt.Errorf("failed to find environment variable '%s': %w", envIdentifier, err)
}
req := &models.EnvironmentVariableUpdateRequest{}
// Use existing key unless --key flag explicitly provides a new one
if cmd.Flags().Changed("key") {
key, _ := cmd.Flags().GetString("key")
req.Key = &key
} else {
req.Key = &existingEnv.Key
}
if cmd.Flags().Changed("value") {
value, _ := cmd.Flags().GetString("value")
req.Value = &value
@@ -54,12 +69,19 @@ func NewUpdateEnvCommand() *cobra.Command {
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
req.IsMultiline = &isMultiline
}
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsPreview == nil && req.IsLiteral == nil && req.IsMultiline == nil {
return fmt.Errorf("at least one field must be provided to update")
if cmd.Flags().Changed("runtime") {
isRuntime, _ := cmd.Flags().GetBool("runtime")
req.IsRuntime = &isRuntime
}
if cmd.Flags().Changed("comment") {
comment, _ := cmd.Flags().GetString("comment")
req.Comment = &comment
}
if req.Value == nil {
return fmt.Errorf("--value is required")
}
appSvc := service.NewApplicationService(client)
env, err := appSvc.UpdateEnv(ctx, appUUID, req)
if err != nil {
return fmt.Errorf("failed to update environment variable: %w", err)
@@ -70,11 +92,13 @@ 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().String("key", "", "New environment variable key (rename)")
cmd.Flags().String("value", "", "New environment variable value (required)")
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)")
cmd.Flags().String("comment", "", "Comment for the environment variable")
return cmd
}
+3 -3
View File
@@ -1,13 +1,13 @@
package application
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewGetCommand() *cobra.Command {
@@ -17,7 +17,7 @@ func NewGetCommand() *cobra.Command {
Long: `Retrieve detailed information about a specific application.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
+4 -4
View File
@@ -1,14 +1,14 @@
package application
import (
"context"
"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"
"github.com/spf13/cobra"
)
func NewListCommand() *cobra.Command {
@@ -16,8 +16,8 @@ func NewListCommand() *cobra.Command {
Use: "list",
Short: "List all applications",
Long: `List all applications in Coolify.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
+3 -3
View File
@@ -1,7 +1,6 @@
package application
import (
"context"
"fmt"
"os"
"os/signal"
@@ -9,9 +8,10 @@ import (
"syscall"
"time"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewLogsCommand() *cobra.Command {
@@ -21,7 +21,7 @@ func NewLogsCommand() *cobra.Command {
Long: `Retrieve logs for an application. Use --follow to continuously stream new logs.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
+3 -3
View File
@@ -1,12 +1,12 @@
package application
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewRestartCommand() *cobra.Command {
@@ -16,7 +16,7 @@ func NewRestartCommand() *cobra.Command {
Long: `Restart a running application.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
+3 -3
View File
@@ -1,12 +1,12 @@
package application
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewStartCommand() *cobra.Command {
@@ -17,7 +17,7 @@ func NewStartCommand() *cobra.Command {
Long: `Start an application (initiates a deployment).`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
+3 -3
View File
@@ -1,12 +1,12 @@
package application
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewStopCommand() *cobra.Command {
@@ -16,7 +16,7 @@ func NewStopCommand() *cobra.Command {
Long: `Stop a running application.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
+96
View File
@@ -0,0 +1,96 @@
package storage
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/service"
)
// NewCreateCommand returns the storage create command
func NewCreateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create <app_uuid>",
Short: "Create a storage for an application",
Long: `Create a persistent volume or file storage for an application.
Examples:
coolify app storage create <app_uuid> --type persistent --name my-volume --mount-path /data
coolify app storage create <app_uuid> --type persistent --name my-volume --mount-path /data --host-path /var/data
coolify app storage create <app_uuid> --type file --mount-path /app/config.yml --content "key: value"
coolify app storage create <app_uuid> --type file --mount-path /app/data --is-directory --fs-path /app/data`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
storageType, _ := cmd.Flags().GetString("type")
mountPath, _ := cmd.Flags().GetString("mount-path")
if storageType == "" {
return fmt.Errorf("--type is required (persistent or file)")
}
if storageType != "persistent" && storageType != "file" {
return fmt.Errorf("--type must be 'persistent' or 'file'")
}
if mountPath == "" {
return fmt.Errorf("--mount-path is required")
}
req := &models.StorageCreateRequest{
Type: storageType,
MountPath: mountPath,
}
if cmd.Flags().Changed("name") {
val, _ := cmd.Flags().GetString("name")
req.Name = &val
}
if cmd.Flags().Changed("host-path") {
val, _ := cmd.Flags().GetString("host-path")
req.HostPath = &val
}
if cmd.Flags().Changed("content") {
val, _ := cmd.Flags().GetString("content")
req.Content = &val
}
if cmd.Flags().Changed("is-directory") {
val, _ := cmd.Flags().GetBool("is-directory")
req.IsDirectory = &val
}
if cmd.Flags().Changed("fs-path") {
val, _ := cmd.Flags().GetString("fs-path")
req.FsPath = &val
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil {
return err
}
appSvc := service.NewApplicationService(client)
if err := appSvc.CreateStorage(ctx, args[0], req); err != nil {
return fmt.Errorf("failed to create storage: %w", err)
}
fmt.Println("Storage created successfully.")
return nil
},
}
cmd.Flags().String("type", "", "Storage type: 'persistent' or 'file' (required)")
cmd.Flags().String("mount-path", "", "Mount path inside the container (required)")
cmd.Flags().String("name", "", "Volume name (persistent only)")
cmd.Flags().String("host-path", "", "Host path (persistent only)")
cmd.Flags().String("content", "", "File content (file only)")
cmd.Flags().Bool("is-directory", false, "Whether this is a directory mount (file only)")
cmd.Flags().String("fs-path", "", "Host directory path (file only, required when --is-directory is set)")
return cmd
}
+43
View File
@@ -0,0 +1,43 @@
package storage
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewDeleteCommand returns the storage delete command
func NewDeleteCommand() *cobra.Command {
return &cobra.Command{
Use: "delete <app_uuid> <storage_uuid>",
Short: "Delete a storage from an application",
Long: `Delete a persistent volume or file storage from an application.
Examples:
coolify app storage delete <app_uuid> <storage_uuid>`,
Args: cli.ExactArgs(2, "<app_uuid> <storage_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil {
return err
}
appSvc := service.NewApplicationService(client)
if err := appSvc.DeleteStorage(ctx, args[0], args[1]); err != nil {
return fmt.Errorf("failed to delete storage: %w", err)
}
fmt.Println("Storage deleted successfully.")
return nil
},
}
}
+51
View File
@@ -0,0 +1,51 @@
package storage
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewListCommand returns the storage list command
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list <app_uuid>",
Short: "List all storages for an application",
Long: `List all persistent volumes and file storages for a specific application.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil {
return err
}
appSvc := service.NewApplicationService(client)
storages, err := appSvc.ListStorages(ctx, args[0])
if err != nil {
return fmt.Errorf("failed to list storages: %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(storages)
},
}
}
+117
View File
@@ -0,0 +1,117 @@
package storage
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/service"
)
// NewUpdateCommand returns the storage update command
func NewUpdateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <app_uuid>",
Short: "Update a storage for an application",
Long: `Update a persistent volume or file storage for an application.
The --uuid and --type flags are required. Use 'coolify app storage list' to find storage UUIDs.
For read-only storages (from docker-compose or services), only --is-preview-suffix-enabled can be updated.
Examples:
coolify app storage update <app_uuid> --uuid <storage_uuid> --type persistent --name my-volume --mount-path /data
coolify app storage update <app_uuid> --uuid <storage_uuid> --type file --content "config content" --mount-path /app/config.yml
coolify app storage update <app_uuid> --uuid <storage_uuid> --type persistent --is-preview-suffix-enabled`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
storageUUID, _ := cmd.Flags().GetString("uuid")
storageID, _ := cmd.Flags().GetInt("id")
storageType, _ := cmd.Flags().GetString("type")
if storageUUID == "" && storageID == 0 {
return fmt.Errorf("--uuid is required (or --id as deprecated fallback)")
}
if storageType == "" {
return fmt.Errorf("--type is required (persistent or file)")
}
if storageType != "persistent" && storageType != "file" {
return fmt.Errorf("--type must be 'persistent' or 'file'")
}
req := &models.StorageUpdateRequest{
Type: storageType,
}
if storageUUID != "" {
req.UUID = &storageUUID
} else {
req.ID = &storageID
}
hasUpdates := false
if cmd.Flags().Changed("is-preview-suffix-enabled") {
val, _ := cmd.Flags().GetBool("is-preview-suffix-enabled")
req.IsPreviewSuffixEnabled = &val
hasUpdates = true
}
if cmd.Flags().Changed("name") {
val, _ := cmd.Flags().GetString("name")
req.Name = &val
hasUpdates = true
}
if cmd.Flags().Changed("mount-path") {
val, _ := cmd.Flags().GetString("mount-path")
req.MountPath = &val
hasUpdates = true
}
if cmd.Flags().Changed("host-path") {
val, _ := cmd.Flags().GetString("host-path")
req.HostPath = &val
hasUpdates = true
}
if cmd.Flags().Changed("content") {
val, _ := cmd.Flags().GetString("content")
req.Content = &val
hasUpdates = true
}
if !hasUpdates {
return fmt.Errorf("no fields to update. Use --help to see available flags")
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil {
return err
}
appSvc := service.NewApplicationService(client)
if err := appSvc.UpdateStorage(ctx, args[0], req); err != nil {
return fmt.Errorf("failed to update storage: %w", err)
}
fmt.Println("Storage updated successfully.")
return nil
},
}
cmd.Flags().String("uuid", "", "Storage UUID (required, use 'storage list' to find)")
cmd.Flags().Int("id", 0, "Storage ID (deprecated, use --uuid instead)")
cmd.Flags().String("type", "", "Storage type: 'persistent' or 'file' (required)")
cmd.Flags().Bool("is-preview-suffix-enabled", false, "Enable preview suffix for this storage")
cmd.Flags().String("name", "", "Storage name (persistent only)")
cmd.Flags().String("mount-path", "", "Mount path inside the container")
cmd.Flags().String("host-path", "", "Host path (persistent only)")
cmd.Flags().String("content", "", "File content (file only)")
return cmd
}
+9 -3
View File
@@ -1,14 +1,14 @@
package application
import (
"context"
"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"
"github.com/spf13/cobra"
)
func NewUpdateCommand() *cobra.Command {
@@ -18,7 +18,7 @@ func NewUpdateCommand() *cobra.Command {
Long: `Update configuration for a specific application. Only specified fields will be updated.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
@@ -104,6 +104,11 @@ func NewUpdateCommand() *cobra.Command {
req.PortsMappings = &ports
hasUpdates = true
}
if cmd.Flags().Changed("dockerfile-target-build") {
targetBuild, _ := cmd.Flags().GetString("dockerfile-target-build")
req.DockerfileTargetBuild = &targetBuild
hasUpdates = true
}
if cmd.Flags().Changed("health-check-enabled") {
enabled, _ := cmd.Flags().GetBool("health-check-enabled")
req.HealthCheckEnabled = &enabled
@@ -152,6 +157,7 @@ func NewUpdateCommand() *cobra.Command {
cmd.Flags().String("dockerfile", "", "Dockerfile content")
cmd.Flags().String("docker-image", "", "Docker image name")
cmd.Flags().String("docker-tag", "", "Docker image tag")
cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage")
cmd.Flags().String("ports-exposes", "", "Exposed ports")
cmd.Flags().String("ports-mappings", "", "Port mappings")
cmd.Flags().Bool("health-check-enabled", false, "Enable health check")
+2 -1
View File
@@ -4,8 +4,9 @@ import (
"fmt"
"os"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
)
func NewCompletionsCommand() *cobra.Command {
+21
View File
@@ -0,0 +1,21 @@
package config
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/config"
)
// NewConfigCommand creates the config command
func NewConfigCommand() *cobra.Command {
return &cobra.Command{
Use: "config",
Short: "Show configuration file location",
Long: "Display the path to the Coolify CLI configuration file",
Run: func(_ *cobra.Command, _ []string) {
fmt.Println(config.Path())
},
}
}
+52
View File
@@ -0,0 +1,52 @@
package config
import (
"strings"
"testing"
"github.com/coollabsio/coolify-cli/internal/config"
)
func TestNewConfigCommand(t *testing.T) {
cmd := NewConfigCommand()
if cmd == nil {
t.Fatal("NewConfigCommand() returned nil")
}
if cmd.Use != "config" {
t.Errorf("Expected Use to be 'config', got '%s'", cmd.Use)
}
if cmd.Short == "" {
t.Error("Short description should not be empty")
}
if cmd.Long == "" {
t.Error("Long description should not be empty")
}
if cmd.Run == nil {
t.Error("Run function should not be nil")
}
}
func TestConfigCommand_Output(t *testing.T) {
// Test that the command returns the expected config path
expectedPath := config.Path()
// The path should not be empty
if expectedPath == "" {
t.Error("Expected config path to not be empty")
}
// The path should end with config.json
if !strings.HasSuffix(expectedPath, "config.json") {
t.Errorf("Expected path to end with 'config.json', got '%s'", expectedPath)
}
// The path should contain the coolify directory
if !strings.Contains(expectedPath, "coolify") {
t.Errorf("Expected path to contain 'coolify', got '%s'", expectedPath)
}
}
+11 -7
View File
@@ -3,10 +3,11 @@ package context
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/config"
)
// NewAddCommand creates the add command
@@ -16,7 +17,7 @@ func NewAddCommand() *cobra.Command {
Example: `context add myserver https://coolify.example.com your-api-token`,
Args: cli.ExactArgs(3, "<context_name> <url> <token>"),
Short: "Add a new context",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
host := args[1]
token := args[2]
@@ -44,12 +45,14 @@ func NewAddCommand() *cobra.Command {
fmt.Printf("%s already exists. Force overwriting.\n", name)
}
viper.Set("instances", instances)
viper.WriteConfig()
return
if err := viper.WriteConfig(); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
return nil
}
fmt.Printf("%s already exists.\n", name)
fmt.Println("\nNote: Use --force to force overwrite.")
return
return nil
}
}
@@ -77,8 +80,9 @@ func NewAddCommand() *cobra.Command {
viper.Set("instances", instances)
if err := viper.WriteConfig(); err != nil {
fmt.Printf("failed to write config: %v\n", err)
return fmt.Errorf("failed to write config: %w", err)
}
return nil
},
}
+1
View File
@@ -22,6 +22,7 @@ func NewContextCommand() *cobra.Command {
cmd.AddCommand(NewSetTokenCommand())
cmd.AddCommand(NewSetDefaultCommand())
cmd.AddCommand(NewVersionCommand())
cmd.AddCommand(NewVerifyCommand())
return cmd
}
+12 -7
View File
@@ -2,11 +2,12 @@ package context
import (
"fmt"
"slices"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"slices"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewDeleteCommand creates the delete command
@@ -17,7 +18,7 @@ func NewDeleteCommand() *cobra.Command {
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Delete a context",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
Name := args[0]
instances := viper.Get("instances").([]interface{})
for i, instance := range instances {
@@ -25,13 +26,17 @@ func NewDeleteCommand() *cobra.Command {
if instanceMap["name"] == Name {
instances = slices.Delete(instances, i, i+1)
viper.Set("instances", instances)
viper.WriteConfig()
if err := viper.WriteConfig(); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
if instanceMap["default"] == true {
if len(instances) > 0 {
instances[0].(map[string]interface{})["default"] = true
viper.Set("instances", instances)
viper.WriteConfig()
if err := viper.WriteConfig(); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
newDefaultName := instances[0].(map[string]interface{})["name"]
fmt.Printf("Context '%s' deleted. '%s' is now the default context.\n", Name, newDefaultName)
} else {
@@ -40,10 +45,10 @@ func NewDeleteCommand() *cobra.Command {
} else {
fmt.Printf("Context '%s' deleted.\n", Name)
}
return
return nil
}
}
fmt.Printf("Context '%s' not found.\n", Name)
return fmt.Errorf("context '%s' not found", Name)
},
}
}
+3 -2
View File
@@ -3,11 +3,12 @@ package context
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/config"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// NewGetCommand creates the get command
+4 -3
View File
@@ -1,10 +1,11 @@
package context
import (
"github.com/coollabsio/coolify-cli/internal/config"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/internal/config"
"github.com/coollabsio/coolify-cli/internal/output"
)
// NewListCommand creates the list command
@@ -12,7 +13,7 @@ func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all configured contexts",
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
// Get instances from viper (returns []interface{})
instancesRaw := viper.Get("instances")
if instancesRaw == nil {
+23 -11
View File
@@ -3,9 +3,10 @@ package context
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewSetTokenCommand creates the set-token command
@@ -18,13 +19,20 @@ func NewSetDefaultCommand() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
instances := viper.Get("instances").([]interface{})
raw := viper.Get("instances")
instances, ok := raw.([]interface{})
if !ok {
return fmt.Errorf("invalid instances configuration")
}
// Check if instance exists
var found bool
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == name {
instanceMap, ok := instance.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid instance configuration")
}
if val, ok := instanceMap["name"].(string); ok && val == name {
found = true
instanceMap["default"] = true
}
@@ -32,13 +40,17 @@ func NewSetDefaultCommand() *cobra.Command {
if !found {
return fmt.Errorf("Context '%s' not found", name)
} else {
// Only unset other defaults if we found the target instance
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] != name {
instanceMap["default"] = false
}
}
// Only unset other defaults if we found the target instance
for _, instance := range instances {
instanceMap, ok := instance.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid instance configuration")
}
if val, ok := instanceMap["name"].(string); ok && val != name {
instanceMap["default"] = false
}
}
+8 -5
View File
@@ -3,9 +3,10 @@ package context
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewSetTokenCommand creates the set-token command
@@ -15,7 +16,7 @@ func NewSetTokenCommand() *cobra.Command {
Example: `context set-token myserver your-new-api-token`,
Args: cli.ExactArgs(2, "<context_name> <token>"),
Short: "Update the API token for a context",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
name := args[0]
token := args[1]
var found interface{}
@@ -27,8 +28,7 @@ func NewSetTokenCommand() *cobra.Command {
}
}
if found == nil {
fmt.Printf("Context '%s' not found.\n", name)
return
return fmt.Errorf("context '%s' not found", name)
}
instances := viper.Get("instances").([]interface{})
for _, instance := range instances {
@@ -38,8 +38,11 @@ func NewSetTokenCommand() *cobra.Command {
}
}
viper.Set("instances", instances)
viper.WriteConfig()
if err := viper.WriteConfig(); err != nil {
return fmt.Errorf("failed to update token for context '%s': %w", name, err)
}
fmt.Printf("Token updated for context '%s'.\n", name)
return nil
},
}
}
+9 -13
View File
@@ -3,9 +3,10 @@ package context
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewUpdateCommand creates the update command
@@ -15,7 +16,7 @@ func NewUpdateCommand() *cobra.Command {
Example: `context update myserver --name newname --url https://new.coolify.com --token newtoken`,
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Update a context's properties (name, URL, token)",
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
oldName := args[0]
instances := viper.Get("instances").([]interface{})
@@ -26,9 +27,7 @@ func NewUpdateCommand() *cobra.Command {
// Check if at least one flag is provided
if newName == "" && newURL == "" && newToken == "" {
fmt.Println("Error: At least one of --name, --url, or --token must be provided")
fmt.Println("\nUsage: coolify context update <context_name> [--name <new_name>] [--url <new_url>] [--token <new_token>]")
return
return fmt.Errorf("at least one of --name, --url, or --token must be provided")
}
// Find the context
@@ -44,8 +43,7 @@ func NewUpdateCommand() *cobra.Command {
}
if !found {
fmt.Printf("Context '%s' not found.\n", oldName)
return
return fmt.Errorf("context '%s' not found", oldName)
}
// If renaming, check if new name already exists
@@ -53,8 +51,7 @@ func NewUpdateCommand() *cobra.Command {
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == newName {
fmt.Printf("Error: Context with name '%s' already exists.\n", newName)
return
return fmt.Errorf("context with name '%s' already exists", newName)
}
}
contextToUpdate["name"] = newName
@@ -72,10 +69,8 @@ func NewUpdateCommand() *cobra.Command {
// Save changes
viper.Set("instances", instances)
err := viper.WriteConfig()
if err != nil {
fmt.Printf("Error saving config: %v\n", err)
return
if err := viper.WriteConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
// Use the new name if renamed, otherwise use old name
@@ -84,6 +79,7 @@ func NewUpdateCommand() *cobra.Command {
finalName = newName
}
fmt.Printf("Context '%s' updated successfully.\n", finalName)
return nil
},
}
+19 -7
View File
@@ -3,9 +3,10 @@ package context
import (
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewUseCommand creates the use command
@@ -15,15 +16,22 @@ func NewUseCommand() *cobra.Command {
Example: `context use myserver`,
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Switch to a different context (set as default)",
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(_ *cobra.Command, args []string) error {
name := args[0]
instances := viper.Get("instances").([]interface{})
raw := viper.Get("instances")
instances, ok := raw.([]interface{})
if !ok {
return fmt.Errorf("invalid instances configuration")
}
// Check if instance exists
var found bool
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == name {
instanceMap, ok := instance.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid instance configuration")
}
if val, ok := instanceMap["name"].(string); ok && val == name {
found = true
break
}
@@ -35,8 +43,12 @@ func NewUseCommand() *cobra.Command {
// Update default
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == name {
instanceMap, ok := instance.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid instance configuration")
}
if val, ok := instanceMap["name"].(string); ok && val == name {
instanceMap["default"] = true
} else {
delete(instanceMap, "default")
+41
View File
@@ -0,0 +1,41 @@
package context
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewVerifyCommand creates the verify command for contexts
func NewVerifyCommand() *cobra.Command {
return &cobra.Command{
Use: "verify",
Short: "Verify current context connection and authentication",
Long: `Verify that the current context is properly configured by testing the connection
to the Coolify instance and validating the API token.`,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
// Get API client - this will use the current default context
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Try to get version - this verifies both connection and authentication
version, err := client.GetVersion(ctx)
if err != nil {
return fmt.Errorf("verification failed: %w", err)
}
// If we got here, connection and authentication are working
fmt.Printf("✓ Connection successful\n")
fmt.Printf("✓ Authentication valid\n")
fmt.Printf("✓ Coolify version: %s\n", version)
return nil
},
}
}
+106
View File
@@ -0,0 +1,106 @@
package context
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coollabsio/coolify-cli/internal/api"
)
// TestVerifyCommand_APIIntegration tests the verify logic using the API client directly
// This tests the core functionality that the verify command relies on
func TestVerifyCommand_APIIntegration(t *testing.T) {
t.Run("successful verification", func(t *testing.T) {
// Create a test HTTP server that responds to /api/v1/version
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/v1/version", r.URL.Path)
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("4.0.0-beta.383"))
}))
defer server.Close()
// Create API client and verify connection
client := api.NewClient(server.URL, "test-token")
version, err := client.GetVersion(context.Background())
// Verify results
require.NoError(t, err)
assert.Equal(t, "4.0.0-beta.383", version)
})
t.Run("unauthorized - invalid token", func(t *testing.T) {
// Create a test HTTP server that returns 401
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_ = json.NewEncoder(w).Encode(map[string]string{
"message": "Invalid token",
})
}))
defer server.Close()
// Create API client with invalid token
client := api.NewClient(server.URL, "invalid-token")
_, err := client.GetVersion(context.Background())
// Verify error
require.Error(t, err)
assert.True(t, api.IsUnauthorized(err))
})
t.Run("server error", func(t *testing.T) {
// Create a test HTTP server that returns 500
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]string{
"error": "Internal server error",
})
}))
defer server.Close()
// Create API client
client := api.NewClient(server.URL, "test-token", api.WithRetries(0))
_, err := client.GetVersion(context.Background())
// Verify error
require.Error(t, err)
var apiErr *api.Error
require.ErrorAs(t, err, &apiErr)
assert.Equal(t, 500, apiErr.StatusCode)
})
t.Run("not found", func(t *testing.T) {
// Create a test HTTP server that returns 404
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_ = json.NewEncoder(w).Encode(map[string]string{
"message": "Endpoint not found",
})
}))
defer server.Close()
// Create API client
client := api.NewClient(server.URL, "test-token")
_, err := client.GetVersion(context.Background())
// Verify error
require.Error(t, err)
assert.True(t, api.IsNotFound(err))
})
}
// TestNewVerifyCommand tests that the command is properly configured
func TestNewVerifyCommand(t *testing.T) {
cmd := NewVerifyCommand()
assert.Equal(t, "verify", cmd.Use)
assert.NotEmpty(t, cmd.Short)
assert.NotEmpty(t, cmd.Long)
assert.NotNil(t, cmd.RunE)
}
+4 -4
View File
@@ -1,11 +1,11 @@
package context
import (
"context"
"fmt"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewVersionCommand creates the version command for contexts
@@ -13,8 +13,8 @@ func NewVersionCommand() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Get current context's Coolify version",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
// Get API client
client, err := cli.GetAPIClient(cmd)
+8 -3
View File
@@ -1,14 +1,14 @@
package backup
import (
"context"
"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"
"github.com/spf13/cobra"
)
// NewCreateCommand creates a new database
@@ -21,7 +21,7 @@ func NewCreateCommand() *cobra.Command {
Example: coolify database backup create abc123 --frequency "0 0 * * *" --enabled`,
Args: cli.ExactArgs(1, "<database_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
dbUUID := args[0]
client, err := cli.GetAPIClient(cmd)
@@ -29,6 +29,11 @@ Example: coolify database backup create abc123 --frequency "0 0 * * *" --enabled
return fmt.Errorf("failed to get API client: %w", err)
}
// Check minimum version requirement
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.436"); err != nil {
return err
}
req := &models.DatabaseBackupCreateRequest{}
// Apply flags if provided
+3 -3
View File
@@ -2,14 +2,14 @@ package backup
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewDeleteExecutionCommand lists all databases
@@ -20,7 +20,7 @@ func NewDeleteExecutionCommand() *cobra.Command {
Long: `Delete a specific backup execution and optionally from S3. First UUID is the database, second is the backup configuration, third is the specific execution.`,
Args: cli.ExactArgs(3, "<database_uuid> <backup_uuid> <execution_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
dbUUID := args[0]
backupUUID := args[1]
executionUUID := args[2]
+3 -3
View File
@@ -2,14 +2,14 @@ package backup
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewDeleteCommand deletes a database
@@ -20,7 +20,7 @@ func NewDeleteCommand() *cobra.Command {
Long: `Delete a backup configuration and optionally all its executions from S3. First UUID is the database, second is the specific backup configuration.`,
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
dbUUID := args[0]
backupUUID := args[1]
+3 -3
View File
@@ -1,13 +1,13 @@
package backup
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewExecutionCommand lists all databases
@@ -18,7 +18,7 @@ func NewExecutionCommand() *cobra.Command {
Long: `List all executions for a backup configuration. First UUID is the database, second is the specific backup configuration.`,
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
dbUUID := args[0]
backupUUID := args[1]
+3 -3
View File
@@ -1,13 +1,13 @@
package backup
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewListCommand lists all databases
@@ -18,7 +18,7 @@ func NewListCommand() *cobra.Command {
Long: `List all backup configurations for a specific database.`,
Args: cli.ExactArgs(1, "<database_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
dbUUID := args[0]
client, err := cli.GetAPIClient(cmd)
+3 -3
View File
@@ -1,13 +1,13 @@
package backup
import (
"context"
"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/service"
"github.com/spf13/cobra"
)
// NewTriggerCommand triggers a database backup
@@ -18,7 +18,7 @@ func NewTriggerCommand() *cobra.Command {
Long: `Trigger an immediate backup for a specific backup configuration. First UUID is the database, second is the specific backup configuration to trigger.`,
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
dbUUID := args[0]
backupUUID := args[1]
+3 -3
View File
@@ -1,13 +1,13 @@
package backup
import (
"context"
"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/service"
"github.com/spf13/cobra"
)
// NewUpdateCommand updates a database
@@ -18,7 +18,7 @@ func NewUpdateCommand() *cobra.Command {
Long: `Update a backup configuration settings (frequency, retention, S3, etc.). First UUID is the database, second is the specific backup configuration.`,
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
dbUUID := args[0]
backupUUID := args[1]
+4 -4
View File
@@ -1,15 +1,15 @@
package database
import (
"context"
"fmt"
"strings"
"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"
"github.com/spf13/cobra"
)
func NewCreateCommand() *cobra.Command {
@@ -25,7 +25,7 @@ Examples:
coolify databases create mysql --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production --name="My MySQL"`,
Args: cli.ExactArgs(1, "<type>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
dbType := args[0]
validTypes := []string{"postgresql", "mysql", "mariadb", "mongodb", "redis", "keydb", "clickhouse", "dragonfly"}
isValid := false
@@ -116,7 +116,7 @@ Examples:
}
if cmd.Flags().Changed("postgres-db") {
db, _ := cmd.Flags().GetString("postgres-db")
req.PostgresDb = &db
req.PostgresDB = &db
}
}
+28
View File
@@ -4,6 +4,8 @@ import (
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/cmd/database/backup"
"github.com/coollabsio/coolify-cli/cmd/database/env"
"github.com/coollabsio/coolify-cli/cmd/database/storage"
)
// NewDatabaseCommand creates the database parent command with all subcommands
@@ -25,6 +27,19 @@ func NewDatabaseCommand() *cobra.Command {
cmd.AddCommand(NewUpdateCommand())
cmd.AddCommand(NewDeleteCommand())
// Add env subcommand
envCmd := &cobra.Command{
Use: "env",
Short: "Manage database 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)
// Add backup subcommand
backupCmd := &cobra.Command{
Use: "backup",
@@ -39,5 +54,18 @@ func NewDatabaseCommand() *cobra.Command {
backupCmd.AddCommand(backup.NewDeleteExecutionCommand())
cmd.AddCommand(backupCmd)
// Add storage subcommand
storageCmd := &cobra.Command{
Use: "storage",
Aliases: []string{"storages"},
Short: "Manage database storages",
Long: `List and manage persistent volumes and file storages for databases.`,
}
storageCmd.AddCommand(storage.NewListCommand())
storageCmd.AddCommand(storage.NewCreateCommand())
storageCmd.AddCommand(storage.NewUpdateCommand())
storageCmd.AddCommand(storage.NewDeleteCommand())
cmd.AddCommand(storageCmd)
return cmd
}
+3 -3
View File
@@ -2,14 +2,14 @@ package database
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewDeleteCommand deletes a database
@@ -20,7 +20,7 @@ func NewDeleteCommand() *cobra.Command {
Long: `Delete a database and optionally clean up its configurations, volumes, and networks.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
force, _ := cmd.Flags().GetBool("force")
+79
View File
@@ -0,0 +1,79 @@
package env
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/service"
)
func NewCreateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create <database_uuid>",
Short: "Create an environment variable for a database",
Long: `Create a new environment variable for a specific database. Use --key and --value flags to specify the variable.`,
Args: cli.ExactArgs(1, "<database_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
key, _ := cmd.Flags().GetString("key")
value, _ := cmd.Flags().GetString("value")
if key == "" {
return fmt.Errorf("--key is required")
}
if value == "" {
return fmt.Errorf("--value is required")
}
req := &models.DatabaseEnvironmentVariableCreateRequest{
Key: key,
Value: value,
}
if cmd.Flags().Changed("is-literal") {
isLiteral, _ := cmd.Flags().GetBool("is-literal")
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("is-multiline") {
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
req.IsMultiline = &isMultiline
}
if cmd.Flags().Changed("is-shown-once") {
isShownOnce, _ := cmd.Flags().GetBool("is-shown-once")
req.IsShownOnce = &isShownOnce
}
if cmd.Flags().Changed("comment") {
comment, _ := cmd.Flags().GetString("comment")
req.Comment = &comment
}
dbSvc := service.NewDatabaseService(client)
_, err = dbSvc.CreateEnv(ctx, uuid, req)
if err != nil {
return fmt.Errorf("failed to create environment variable: %w", err)
}
fmt.Printf("Environment variable '%s' created successfully.\n", key)
return nil
},
}
cmd.Flags().String("key", "", "Environment variable key (required)")
cmd.Flags().String("value", "", "Environment variable value (required)")
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("is-shown-once", false, "Only show value once")
cmd.Flags().String("comment", "", "Comment for the environment variable")
return cmd
}
+56
View File
@@ -0,0 +1,56 @@
package env
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
func NewDeleteCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete <database_uuid> <env_uuid>",
Short: "Delete an environment variable",
Long: `Delete an environment variable from a database. First UUID is the database, second is the specific environment variable to delete.`,
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
dbUUID := args[0]
envUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to delete this environment variable? (yes/no): ")
_, _ = fmt.Scanln(&response)
if response != "yes" && response != "y" {
fmt.Println("Delete cancelled.")
return nil
}
}
dbSvc := service.NewDatabaseService(client)
err = dbSvc.DeleteEnv(ctx, dbUUID, envUUID)
if err != nil {
return fmt.Errorf("failed to delete environment variable: %w", err)
}
fmt.Println("Environment variable deleted successfully.")
return nil
},
}
cmd.Flags().Bool("force", false, "Skip confirmation prompt")
return cmd
}
+57
View File
@@ -0,0 +1,57 @@
package env
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
)
func NewGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <database_uuid> <env_uuid_or_key>",
Short: "Get environment variable details",
Long: `Get detailed information about a specific environment variable. First UUID is the database, second is the environment variable UUID or key name.`,
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
dbUUID := args[0]
envUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbSvc := service.NewDatabaseService(client)
env, err := dbSvc.GetEnv(ctx, dbUUID, envUUID)
if err != nil {
return fmt.Errorf("failed to get environment variable: %w", err)
}
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// Mask sensitive value unless --show-sensitive is used
if !showSensitive {
env.Value = "********"
if env.RealValue != nil {
masked := "********"
env.RealValue = &masked
}
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(env)
},
}
}
+58
View File
@@ -0,0 +1,58 @@
package env
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
)
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list <database_uuid>",
Short: "List all environment variables for a database",
Long: `List all environment variables for a specific database.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbSvc := service.NewDatabaseService(client)
envs, err := dbSvc.ListEnvs(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to list environment variables: %w", err)
}
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// Mask sensitive values unless --show-sensitive is used
if !showSensitive {
for i := range envs {
envs[i].Value = "********"
if envs[i].RealValue != nil {
masked := "********"
envs[i].RealValue = &masked
}
}
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(envs)
},
}
}
+145
View File
@@ -0,0 +1,145 @@
package env
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/parser"
"github.com/coollabsio/coolify-cli/internal/service"
)
func NewSyncCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "sync <database_uuid>",
Short: "Sync environment variables from a .env file",
Long: `Sync environment variables from a .env file. This command intelligently:
- Updates existing environment variables with new values
- Creates new environment variables that don't exist yet
- Uses efficient bulk operations where possible
Example: coolify db env sync abc123 --file .env.production`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
filePath, _ := cmd.Flags().GetString("file")
if filePath == "" {
return fmt.Errorf("--file is required")
}
isLiteral, _ := cmd.Flags().GetBool("is-literal")
// Parse the .env file
envVars, err := parser.ParseEnvFile(filePath)
if err != nil {
return fmt.Errorf("failed to parse .env file: %w", err)
}
if len(envVars) == 0 {
fmt.Println("No environment variables found in file.")
return nil
}
fmt.Printf("Found %d environment variables in file. Syncing...\n", len(envVars))
// Fetch existing environment variables
dbSvc := service.NewDatabaseService(client)
existingEnvs, err := dbSvc.ListEnvs(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to list existing environment variables: %w", err)
}
// Build a map of existing env vars by key
existingMap := make(map[string]models.DatabaseEnvironmentVariable)
for _, env := range existingEnvs {
existingMap[env.Key] = env
}
// Separate into updates and creates
var toUpdate []models.DatabaseEnvironmentVariableCreateRequest
var toCreate []models.DatabaseEnvironmentVariableCreateRequest
for _, envVar := range envVars {
req := models.DatabaseEnvironmentVariableCreateRequest{
Key: envVar.Key,
Value: envVar.Value,
}
// Apply flags if explicitly provided
if cmd.Flags().Changed("is-literal") {
req.IsLiteral = &isLiteral
}
// Auto-detect multiline values
if strings.Contains(envVar.Value, "\n") {
multiline := true
req.IsMultiline = &multiline
}
if _, exists := existingMap[envVar.Key]; exists {
toUpdate = append(toUpdate, req)
} else {
toCreate = append(toCreate, req)
}
}
updateCount := 0
createCount := 0
failCount := 0
// Perform bulk update if there are vars to update
if len(toUpdate) > 0 {
fmt.Printf("Updating %d existing variables...\n", len(toUpdate))
bulkReq := &models.DatabaseEnvBulkUpdateRequest{
Data: toUpdate,
}
_, err := dbSvc.BulkUpdateEnvs(ctx, uuid, bulkReq)
if err != nil {
fmt.Printf(" ✗ Bulk update failed: %v\n", err)
failCount += len(toUpdate)
} else {
updateCount = len(toUpdate)
fmt.Printf(" ✓ Successfully updated %d variables\n", updateCount)
}
}
// Create new variables one by one
if len(toCreate) > 0 {
fmt.Printf("Creating %d new variables...\n", len(toCreate))
for _, req := range toCreate {
_, err := dbSvc.CreateEnv(ctx, uuid, &req)
if err != nil {
fmt.Printf(" ✗ Failed to create '%s': %v\n", req.Key, err)
failCount++
} else {
fmt.Printf(" ✓ Created '%s'\n", req.Key)
createCount++
}
}
}
fmt.Printf("\nSync complete: %d updated, %d created, %d failed\n", updateCount, createCount, failCount)
if failCount > 0 {
return fmt.Errorf("some environment variables failed to sync")
}
return nil
},
}
cmd.Flags().StringP("file", "f", "", "Path to .env file (required)")
cmd.Flags().Bool("is-literal", false, "Treat all values as literal (don't interpolate variables)")
return cmd
}
+95
View File
@@ -0,0 +1,95 @@
package env
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/service"
)
func NewUpdateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <database_uuid> <env_uuid_or_key>",
Short: "Update an environment variable",
Long: `Update an existing environment variable. Identify it by UUID or key name.`,
Args: cli.ExactArgs(2, "<database_uuid> <env_uuid_or_key>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
dbUUID := args[0]
envIdentifier := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Check minimum version requirement
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.469"); err != nil {
return err
}
dbSvc := service.NewDatabaseService(client)
// Look up the env var to resolve its key
existingEnv, err := dbSvc.GetEnv(ctx, dbUUID, envIdentifier)
if err != nil {
return fmt.Errorf("failed to find environment variable '%s': %w", envIdentifier, err)
}
req := &models.DatabaseEnvironmentVariableUpdateRequest{}
// Use existing key unless --key flag explicitly provides a new one
if cmd.Flags().Changed("key") {
key, _ := cmd.Flags().GetString("key")
req.Key = &key
} else {
req.Key = &existingEnv.Key
}
if cmd.Flags().Changed("value") {
value, _ := cmd.Flags().GetString("value")
req.Value = &value
}
if cmd.Flags().Changed("is-literal") {
isLiteral, _ := cmd.Flags().GetBool("is-literal")
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("is-multiline") {
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
req.IsMultiline = &isMultiline
}
if cmd.Flags().Changed("is-shown-once") {
isShownOnce, _ := cmd.Flags().GetBool("is-shown-once")
req.IsShownOnce = &isShownOnce
}
if cmd.Flags().Changed("comment") {
comment, _ := cmd.Flags().GetString("comment")
req.Comment = &comment
}
if req.Value == nil {
return fmt.Errorf("--value is required")
}
env, err := dbSvc.UpdateEnv(ctx, dbUUID, req)
if err != nil {
return fmt.Errorf("failed to update environment variable: %w", err)
}
fmt.Printf("Environment variable '%s' updated successfully.\n", env.Key)
return nil
},
}
cmd.Flags().String("key", "", "New environment variable key (rename)")
cmd.Flags().String("value", "", "New environment variable value (required)")
cmd.Flags().Bool("is-literal", false, "Treat value as literal")
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
cmd.Flags().Bool("is-shown-once", false, "Only show value once")
cmd.Flags().String("comment", "", "Comment for the environment variable")
return cmd
}
+15 -5
View File
@@ -1,13 +1,13 @@
package database
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewGetCommand gets database details
@@ -18,7 +18,7 @@ func NewGetCommand() *cobra.Command {
Long: `Get detailed information about a specific database by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
@@ -32,15 +32,25 @@ func NewGetCommand() *cobra.Command {
return fmt.Errorf("failed to get database: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter("table", output.Options{
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(database)
if err := formatter.Format(database); err != nil {
return err
}
if !showSensitive && format == output.FormatTable {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
}
+19 -6
View File
@@ -1,13 +1,13 @@
package database
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewListCommand lists all databases
@@ -16,8 +16,8 @@ func NewListCommand() *cobra.Command {
Use: "list",
Short: "List all databases",
Long: `List all databases in Coolify.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
@@ -30,12 +30,25 @@ func NewListCommand() *cobra.Command {
return fmt.Errorf("failed to list databases: %w", err)
}
formatter, err := output.NewFormatter("table", output.Options{})
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(databases)
if err := formatter.Format(databases); err != nil {
return err
}
if !showSensitive && format == output.FormatTable {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
}
+3 -3
View File
@@ -1,12 +1,12 @@
package database
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewRestartCommand restarts a database
@@ -17,7 +17,7 @@ func NewRestartCommand() *cobra.Command {
Long: `Restart a database by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
+3 -3
View File
@@ -1,12 +1,12 @@
package database
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewStartCommand starts a database
@@ -17,7 +17,7 @@ func NewStartCommand() *cobra.Command {
Long: `Start a database by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
+3 -3
View File
@@ -1,12 +1,12 @@
package database
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewStopCommand stops a database
@@ -17,7 +17,7 @@ func NewStopCommand() *cobra.Command {
Long: `Stop a database by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
+94
View File
@@ -0,0 +1,94 @@
package storage
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/service"
)
// NewCreateCommand returns the database storage create command
func NewCreateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create <db_uuid>",
Short: "Create a storage for a database",
Long: `Create a persistent volume or file storage for a database.
Examples:
coolify db storage create <db_uuid> --type persistent --name my-volume --mount-path /data
coolify db storage create <db_uuid> --type file --mount-path /app/config.yml --content "key: value"`,
Args: cli.ExactArgs(1, "<db_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
storageType, _ := cmd.Flags().GetString("type")
mountPath, _ := cmd.Flags().GetString("mount-path")
if storageType == "" {
return fmt.Errorf("--type is required (persistent or file)")
}
if storageType != "persistent" && storageType != "file" {
return fmt.Errorf("--type must be 'persistent' or 'file'")
}
if mountPath == "" {
return fmt.Errorf("--mount-path is required")
}
req := &models.StorageCreateRequest{
Type: storageType,
MountPath: mountPath,
}
if cmd.Flags().Changed("name") {
val, _ := cmd.Flags().GetString("name")
req.Name = &val
}
if cmd.Flags().Changed("host-path") {
val, _ := cmd.Flags().GetString("host-path")
req.HostPath = &val
}
if cmd.Flags().Changed("content") {
val, _ := cmd.Flags().GetString("content")
req.Content = &val
}
if cmd.Flags().Changed("is-directory") {
val, _ := cmd.Flags().GetBool("is-directory")
req.IsDirectory = &val
}
if cmd.Flags().Changed("fs-path") {
val, _ := cmd.Flags().GetString("fs-path")
req.FsPath = &val
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil {
return err
}
dbSvc := service.NewDatabaseService(client)
if err := dbSvc.CreateStorage(ctx, args[0], req); err != nil {
return fmt.Errorf("failed to create storage: %w", err)
}
fmt.Println("Storage created successfully.")
return nil
},
}
cmd.Flags().String("type", "", "Storage type: 'persistent' or 'file' (required)")
cmd.Flags().String("mount-path", "", "Mount path inside the container (required)")
cmd.Flags().String("name", "", "Volume name (persistent only)")
cmd.Flags().String("host-path", "", "Host path (persistent only)")
cmd.Flags().String("content", "", "File content (file only)")
cmd.Flags().Bool("is-directory", false, "Whether this is a directory mount (file only)")
cmd.Flags().String("fs-path", "", "Host directory path (file only, required when --is-directory is set)")
return cmd
}
+43
View File
@@ -0,0 +1,43 @@
package storage
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewDeleteCommand returns the database storage delete command
func NewDeleteCommand() *cobra.Command {
return &cobra.Command{
Use: "delete <db_uuid> <storage_uuid>",
Short: "Delete a storage from a database",
Long: `Delete a persistent volume or file storage from a database.
Examples:
coolify db storage delete <db_uuid> <storage_uuid>`,
Args: cli.ExactArgs(2, "<db_uuid> <storage_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil {
return err
}
dbSvc := service.NewDatabaseService(client)
if err := dbSvc.DeleteStorage(ctx, args[0], args[1]); err != nil {
return fmt.Errorf("failed to delete storage: %w", err)
}
fmt.Println("Storage deleted successfully.")
return nil
},
}
}
+51
View File
@@ -0,0 +1,51 @@
package storage
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewListCommand returns the database storage list command
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list <db_uuid>",
Short: "List all storages for a database",
Long: `List all persistent volumes and file storages for a specific database.`,
Args: cli.ExactArgs(1, "<db_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil {
return err
}
dbSvc := service.NewDatabaseService(client)
storages, err := dbSvc.ListStorages(ctx, args[0])
if err != nil {
return fmt.Errorf("failed to list storages: %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(storages)
},
}
}
+114
View File
@@ -0,0 +1,114 @@
package storage
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/service"
)
// NewUpdateCommand returns the database storage update command
func NewUpdateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <db_uuid>",
Short: "Update a storage for a database",
Long: `Update a persistent volume or file storage for a database.
The --uuid and --type flags are required. Use 'coolify db storage list' to find storage UUIDs.
Examples:
coolify db storage update <db_uuid> --uuid <storage_uuid> --type persistent --name my-volume
coolify db storage update <db_uuid> --uuid <storage_uuid> --type persistent --is-preview-suffix-enabled`,
Args: cli.ExactArgs(1, "<db_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
storageUUID, _ := cmd.Flags().GetString("uuid")
storageID, _ := cmd.Flags().GetInt("id")
storageType, _ := cmd.Flags().GetString("type")
if storageUUID == "" && storageID == 0 {
return fmt.Errorf("--uuid is required (or --id as deprecated fallback)")
}
if storageType == "" {
return fmt.Errorf("--type is required (persistent or file)")
}
if storageType != "persistent" && storageType != "file" {
return fmt.Errorf("--type must be 'persistent' or 'file'")
}
req := &models.StorageUpdateRequest{
Type: storageType,
}
if storageUUID != "" {
req.UUID = &storageUUID
} else {
req.ID = &storageID
}
hasUpdates := false
if cmd.Flags().Changed("is-preview-suffix-enabled") {
val, _ := cmd.Flags().GetBool("is-preview-suffix-enabled")
req.IsPreviewSuffixEnabled = &val
hasUpdates = true
}
if cmd.Flags().Changed("name") {
val, _ := cmd.Flags().GetString("name")
req.Name = &val
hasUpdates = true
}
if cmd.Flags().Changed("mount-path") {
val, _ := cmd.Flags().GetString("mount-path")
req.MountPath = &val
hasUpdates = true
}
if cmd.Flags().Changed("host-path") {
val, _ := cmd.Flags().GetString("host-path")
req.HostPath = &val
hasUpdates = true
}
if cmd.Flags().Changed("content") {
val, _ := cmd.Flags().GetString("content")
req.Content = &val
hasUpdates = true
}
if !hasUpdates {
return fmt.Errorf("no fields to update. Use --help to see available flags")
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil {
return err
}
dbSvc := service.NewDatabaseService(client)
if err := dbSvc.UpdateStorage(ctx, args[0], req); err != nil {
return fmt.Errorf("failed to update storage: %w", err)
}
fmt.Println("Storage updated successfully.")
return nil
},
}
cmd.Flags().String("uuid", "", "Storage UUID (required, use 'storage list' to find)")
cmd.Flags().Int("id", 0, "Storage ID (deprecated, use --uuid instead)")
cmd.Flags().String("type", "", "Storage type: 'persistent' or 'file' (required)")
cmd.Flags().Bool("is-preview-suffix-enabled", false, "Enable preview suffix for this storage")
cmd.Flags().String("name", "", "Storage name (persistent only)")
cmd.Flags().String("mount-path", "", "Mount path inside the container")
cmd.Flags().String("host-path", "", "Host path (persistent only)")
cmd.Flags().String("content", "", "File content (file only)")
return cmd
}
+3 -3
View File
@@ -1,13 +1,13 @@
package database
import (
"context"
"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/service"
"github.com/spf13/cobra"
)
// NewUpdateCommand updates a database
@@ -18,7 +18,7 @@ func NewUpdateCommand() *cobra.Command {
Long: `Update a database's configuration by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
req := &models.DatabaseUpdateRequest{}
+8 -6
View File
@@ -1,13 +1,13 @@
package deployment
import (
"context"
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewBatchCommand deploys multiple resources by name
@@ -20,13 +20,16 @@ Provide resource names as comma-separated values.
Example: coolify deploy batch app1,app2,app3`,
Args: cli.ExactArgs(1, "<names>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
namesStr := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
if err := validateDeployFlags(ctx, cmd, client); err != nil {
return err
}
// Parse comma-separated names
names := make([]string, 0)
@@ -66,7 +69,6 @@ Example: coolify deploy batch app1,app2,app3`,
}
// Deploy all resources
force, _ := cmd.Flags().GetBool("force")
deploySvc := service.NewDeploymentService(client)
type result struct {
@@ -83,7 +85,7 @@ Example: coolify deploy batch app1,app2,app3`,
uuid := nameToUUID[name]
fmt.Printf("Deploying %s...\n", name)
res, err := deploySvc.Deploy(ctx, uuid, force)
res, err := deploySvc.Deploy(ctx, getDeployRequest(cmd, uuid))
if err != nil {
results = append(results, result{
Name: name,
@@ -126,6 +128,6 @@ Example: coolify deploy batch app1,app2,app3`,
},
}
cmd.Flags().Bool("force", false, "Force deployment")
addDeployFlags(cmd)
return cmd
}
+21 -7
View File
@@ -1,13 +1,13 @@
package deployment
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewCancelCommand cancels a deployment
@@ -18,7 +18,7 @@ func NewCancelCommand() *cobra.Command {
Long: `Cancel an in-progress deployment. This will stop the deployment process and clean up any temporary resources.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
@@ -26,13 +26,23 @@ func NewCancelCommand() *cobra.Command {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
// Check minimum version requirement
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.436"); err != nil {
return err
}
force, err := cmd.Flags().GetBool("force")
if err != nil {
return fmt.Errorf("failed to parse force flag: %w", err)
}
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to cancel deployment %s? (yes/no): ", uuid)
fmt.Scanln(&response)
var response string
if _, err := fmt.Scanln(&response); err != nil {
return fmt.Errorf("failed to read confirmation: %w", err)
}
if response != "yes" && response != "y" {
fmt.Println("Cancel aborted.")
@@ -46,7 +56,11 @@ func NewCancelCommand() *cobra.Command {
return fmt.Errorf("failed to cancel deployment: %w", err)
}
format, _ := cmd.Flags().GetString("format")
format, err := cmd.Flags().GetString("format")
if err != nil {
return fmt.Errorf("failed to get format flag: %w", err)
}
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
+46 -1
View File
@@ -1,6 +1,16 @@
package deployment
import "github.com/spf13/cobra"
import (
"context"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/api"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
)
const dockerTagMinVersion = "4.0.0-beta.471"
// NewDeploymentCommand creates the deployment parent command with all subcommands
func NewDeploymentCommand() *cobra.Command {
@@ -19,3 +29,38 @@ func NewDeploymentCommand() *cobra.Command {
return cmd
}
func addDeployFlags(cmd *cobra.Command) {
cmd.Flags().Bool("force", false, "Force deployment")
cmd.Flags().Int("pull-request-id", 0, "Pull request ID for preview deployments")
cmd.Flags().String("docker-tag", "", "Docker image tag override for the deployment")
}
func getDeployRequest(cmd *cobra.Command, uuid string) models.DeployRequest {
req := models.DeployRequest{
UUID: uuid,
}
if cmd.Flags().Changed("force") {
force, _ := cmd.Flags().GetBool("force")
req.Force = &force
}
if cmd.Flags().Changed("pull-request-id") {
pullRequestID, _ := cmd.Flags().GetInt("pull-request-id")
req.PullRequestID = &pullRequestID
}
if cmd.Flags().Changed("docker-tag") {
dockerTag, _ := cmd.Flags().GetString("docker-tag")
req.DockerTag = &dockerTag
}
return req
}
func validateDeployFlags(ctx context.Context, cmd *cobra.Command, client *api.Client) error {
if cmd.Flags().Changed("docker-tag") {
return cli.CheckMinimumVersion(ctx, client, dockerTagMinVersion)
}
return nil
}
+3 -3
View File
@@ -1,13 +1,13 @@
package deployment
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewGetCommand gets deployment details
@@ -18,7 +18,7 @@ func NewGetCommand() *cobra.Command {
Long: `Get detailed information about a specific deployment by its UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
+4 -4
View File
@@ -1,13 +1,13 @@
package deployment
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewListCommand lists all deployments
@@ -16,8 +16,8 @@ func NewListCommand() *cobra.Command {
Use: "list",
Short: "List all deployments",
Long: `List all currently running deployments across all resources.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
+8 -6
View File
@@ -1,13 +1,13 @@
package deployment
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewNameCommand deploys a resource by name
@@ -17,13 +17,16 @@ func NewNameCommand() *cobra.Command {
Short: "Deploy by resource name",
Args: cli.ExactArgs(1, "<resource_name>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
name := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
if err := validateDeployFlags(ctx, cmd, client); err != nil {
return err
}
// Find resource by name
resourceSvc := service.NewResourceService(client)
@@ -45,9 +48,8 @@ func NewNameCommand() *cobra.Command {
}
// Deploy using the found UUID
force, _ := cmd.Flags().GetBool("force")
deploySvc := service.NewDeploymentService(client)
result, err := deploySvc.Deploy(ctx, matchedUUID, force)
result, err := deploySvc.Deploy(ctx, getDeployRequest(cmd, matchedUUID))
if err != nil {
return fmt.Errorf("failed to deploy resource: %w", err)
}
@@ -74,6 +76,6 @@ func NewNameCommand() *cobra.Command {
},
}
cmd.Flags().Bool("force", false, "Force deployment")
addDeployFlags(cmd)
return cmd
}
+8 -6
View File
@@ -1,13 +1,13 @@
package deployment
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// ResultDisplay represents a deploy result for table display
@@ -23,17 +23,19 @@ func NewUUIDCommand() *cobra.Command {
Short: "Deploy by uuid",
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
if err := validateDeployFlags(ctx, cmd, client); err != nil {
return err
}
force, _ := cmd.Flags().GetBool("force")
deploySvc := service.NewDeploymentService(client)
result, err := deploySvc.Deploy(ctx, uuid, force)
result, err := deploySvc.Deploy(ctx, getDeployRequest(cmd, uuid))
if err != nil {
return fmt.Errorf("failed to deploy resource: %w", err)
}
@@ -60,6 +62,6 @@ func NewUUIDCommand() *cobra.Command {
},
}
cmd.Flags().Bool("force", false, "Force deployment")
addDeployFlags(cmd)
return cmd
}
+535 -4
View File
@@ -4,9 +4,12 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
"github.com/spf13/pflag"
)
var docsCmd = &cobra.Command{
@@ -23,11 +26,11 @@ var manCmd = &cobra.Command{
The man pages will be written to the specified directory (default: ./man).`,
Example: ` coolify docs man
coolify docs man --output-dir=/usr/local/share/man/man1`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
outputDir, _ := cmd.Flags().GetString("output-dir")
// Create output directory if it doesn't exist
if err := os.MkdirAll(outputDir, 0755); err != nil {
if err := os.MkdirAll(outputDir, 0750); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
@@ -65,11 +68,11 @@ var markdownCmd = &cobra.Command{
The markdown files will be written to the specified directory (default: ./docs).`,
Example: ` coolify docs markdown
coolify docs markdown --output-dir=./documentation`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
outputDir, _ := cmd.Flags().GetString("output-dir")
// Create output directory if it doesn't exist
if err := os.MkdirAll(outputDir, 0755); err != nil {
if err := os.MkdirAll(outputDir, 0750); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
@@ -85,12 +88,540 @@ The markdown files will be written to the specified directory (default: ./docs).
},
}
var llmsCmd = &cobra.Command{
Use: "llms",
Short: "Generate llms.txt and llms-full.txt for AI agents",
Long: `Generate AI-friendly documentation files for the Coolify CLI.
This creates a concise llms.txt quick reference plus a complete llms-full.txt command catalog.
The output files will be written to the specified paths (defaults: ./llms.txt and ./llms-full.txt).`,
Example: ` coolify docs llms
coolify docs llms --output=./llms.txt --full-output=./llms-full.txt`,
RunE: func(cmd *cobra.Command, _ []string) error {
outputFile, _ := cmd.Flags().GetString("output")
fullOutputFile, _ := cmd.Flags().GetString("full-output")
return writeLLMsArtifacts(outputFile, fullOutputFile)
},
}
const llmsQuickTemplate = `# Coolify CLI - llms.txt
> Quick AI/LLM instructions for the Coolify CLI.
> Source: https://github.com/coollabsio/coolify-cli
> API Spec: https://github.com/coollabsio/coolify/blob/v4.x/openapi.json
## Operating Rules
- Prefer ` + "`--format json`" + ` for automation and parsing.
- Use Coolify UUIDs for resources; do not use internal numeric IDs.
- Team commands are the exception: they use numeric team IDs.
- Authenticate with a saved context when possible; use ` + "`--token`" + ` only for overrides.
- Use ` + "`llms-full.txt`" + ` for the exhaustive command/flag catalog.
## Installation
` + "```bash" + `
# Linux/macOS (recommended)
curl -fsSL https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.sh | bash
# Homebrew (macOS/Linux)
brew install coollabsio/coolify-cli/coolify-cli
# Windows (PowerShell)
irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
# Go install
go install github.com/coollabsio/coolify-cli/coolify@latest
` + "```" + `
## Authentication
1. Get an API token from your Coolify dashboard at ` + "`/security/api-tokens`" + `
2. For Coolify Cloud: ` + "`coolify context set-token cloud <token>`" + `
3. For self-hosted: ` + "`coolify context add -d <context_name> <url> <token>`" + `
4. Switch contexts with ` + "`coolify context use <context_name>`" + `
## Configuration
Config file location:
- Linux/macOS: ` + "`~/.config/coolify/config.json`" + `
- Windows: ` + "`%%APPDATA%%\\coolify\\config.json`" + `
Supports multiple contexts (instances) with ` + "`coolify context`" + ` commands.
## Output Formats
All commands support ` + "`--format`" + ` flag:
- ` + "`table`" + ` (default) - human-readable tabular output
- ` + "`json`" + ` - compact JSON for scripting
- ` + "`pretty`" + ` - indented JSON for debugging
## Global Flags
- ` + "`--context <name>`" + ` - use a specific saved context
- ` + "`--token <token>`" + ` - override token from config
- ` + "`--format table|json|pretty`" + ` - choose output format
- ` + "`--show-sensitive`" + ` - reveal sensitive values
- ` + "`--debug`" + ` - enable debug output
## Common Workflows
### Contexts
` + "```bash" + `
coolify context list
coolify context verify
coolify context version
coolify context use prod
` + "```" + `
### Inventory
` + "```bash" + `
coolify server list
coolify project list
coolify resource list
coolify app list
coolify service list
coolify database list
` + "```" + `
### Applications
` + "```bash" + `
coolify app get <uuid>
coolify app start <uuid>
coolify app stop <uuid>
coolify app restart <uuid>
coolify app logs <uuid> --follow
coolify app deployments list <app-uuid>
coolify app deployments logs <app-uuid> --follow
` + "```" + `
### Environment Variables
` + "```bash" + `
coolify app env list <app-uuid>
coolify app env create <app-uuid> --key API_KEY --value secret123
coolify app env update <app-uuid> <env-uuid-or-key> --value new-secret
coolify app env sync <app-uuid> --file .env.production --build-time --preview
` + "```" + `
### Deployments
` + "```bash" + `
coolify deploy list
coolify deploy name my-application
coolify deploy batch api,worker,frontend --force
coolify deploy cancel <deployment-uuid>
` + "```" + `
### Databases and Services
` + "```bash" + `
coolify database get <uuid>
coolify database create postgresql --server-uuid <uuid> --project-uuid <uuid> --environment-name production
coolify database backup list <database-uuid>
coolify service get <uuid>
coolify service create <type> --project-uuid <uuid> --server-uuid <uuid> --instant-deploy
` + "```" + `
## Common Aliases
- ` + "`coolify app`" + ` | ` + "`coolify apps`" + ` | ` + "`coolify application`" + ` | ` + "`coolify applications`" + `
- ` + "`coolify service`" + ` | ` + "`coolify services`" + ` | ` + "`coolify svc`" + `
- ` + "`coolify database`" + ` | ` + "`coolify databases`" + ` | ` + "`coolify db`" + ` | ` + "`coolify dbs`" + `
- ` + "`coolify teams`" + ` | ` + "`coolify team`" + `
## Full Reference
- Full command and parameter catalog: %s
- Regenerate docs: ` + "`go run ./coolify docs llms`" + `
`
const llmsFullIntro = `# Coolify CLI - llms-full.txt
> Full AI/LLM command catalog for the Coolify CLI.
> Manage Coolify instances (cloud and self-hosted), servers, projects, applications, databases, services, deployments, domains, and private keys.
> Source: https://github.com/coollabsio/coolify-cli
> API Spec: https://github.com/coollabsio/coolify/blob/v4.x/openapi.json
## Companion Files
- Quick instructions: %s
- Regenerate docs: ` + "`go run ./coolify docs llms`" + `
## Installation
` + "```bash" + `
# Linux/macOS (recommended)
curl -fsSL https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.sh | bash
# Homebrew (macOS/Linux)
brew install coollabsio/coolify-cli/coolify-cli
# Windows (PowerShell)
irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
# Go install
go install github.com/coollabsio/coolify-cli/coolify@latest
` + "```" + `
## Authentication
1. Get an API token from your Coolify dashboard at ` + "`/security/api-tokens`" + `
2. For Coolify Cloud: ` + "`coolify context set-token cloud <token>`" + `
3. For self-hosted: ` + "`coolify context add -d <context_name> <url> <token>`" + `
## Configuration
Config file location:
- Linux/macOS: ` + "`~/.config/coolify/config.json`" + `
- Windows: ` + "`%%APPDATA%%\\coolify\\config.json`" + `
Supports multiple contexts (instances) with ` + "`coolify context`" + ` commands.
## Output Formats
All commands support ` + "`--format`" + ` flag:
- ` + "`table`" + ` (default) - human-readable tabular output
- ` + "`json`" + ` - compact JSON for scripting
- ` + "`pretty`" + ` - indented JSON for debugging
`
const llmsFullBody = `
## Supported Database Types
When using ` + "`coolify database create <type>`" + `:
- ` + "`postgresql`" + `
- ` + "`mysql`" + `
- ` + "`mariadb`" + `
- ` + "`mongodb`" + `
- ` + "`redis`" + `
- ` + "`keydb`" + `
- ` + "`clickhouse`" + `
- ` + "`dragonfly`" + `
## Usage Examples
` + "```bash" + `
# Multi-context workflow
coolify context add prod https://prod.coolify.io <token>
coolify context add staging https://staging.coolify.io <token>
coolify context use prod
coolify --context=staging server list
# Application lifecycle
coolify app list
coolify app get <uuid>
coolify app start <uuid>
coolify app stop <uuid>
coolify app restart <uuid>
coolify app logs <uuid> --follow
# Environment variable management
coolify app env list <uuid>
coolify app env create <uuid> --key API_KEY --value secret123
coolify app env sync <uuid> --file .env.production --build-time --preview
# Deploy workflows
coolify deploy name my-application
coolify deploy batch api,worker,frontend --force
coolify deploy list
coolify deploy cancel <uuid>
# Database backup
coolify database backup create <db-uuid> --frequency "0 2 * * *" --enabled --save-s3
coolify database backup trigger <db-uuid> <backup-uuid>
# Application creation
coolify app create public --project-uuid <uuid> --server-uuid <uuid> --git-repository https://github.com/user/repo --git-branch main --build-pack nixpacks --ports-exposes 3000
coolify app create dockerfile --project-uuid <uuid> --server-uuid <uuid> --dockerfile "FROM node:18\nCOPY . .\nRUN npm install\nCMD [\"node\", \"index.js\"]"
coolify app create dockerimage --project-uuid <uuid> --server-uuid <uuid> --docker-registry-image-name nginx --ports-exposes 80
# Service creation (one-click services)
coolify service create <type> --project-uuid <uuid> --server-uuid <uuid> --instant-deploy
coolify service create --list-types # list all available service types
# Storage management
coolify app storage create <app-uuid> --type persistent --mount-path /data --name my-volume
coolify app storage create <app-uuid> --type file --mount-path /app/config.yml --content "key: value"
# GitHub App integration
coolify github list
coolify github repos <app-uuid>
coolify github branches <app-uuid> owner/repo
# Team management
coolify team list
coolify team current
coolify team members list
` + "```" + `
## API Notes
- All resource identifiers use UUIDs (not internal database IDs)
- API base path: ` + "`/api/v1/`" + `
- Authentication: Bearer token via ` + "`--token`" + ` flag or context configuration
- ` + "`app env sync`" + ` behavior: updates existing variables, creates missing ones, does NOT delete variables not in the file
- ` + "`app start`" + ` aliases to ` + "`app deploy`" + ` and also accepts ` + "`--force`" + ` and ` + "`--instant-deploy`" + ` flags
- Deployment logs support ` + "`--follow`" + ` for real-time streaming and ` + "`--debuglogs`" + ` for internal operations
- ` + "`app logs`" + ` defaults to 100 lines; ` + "`app deployments logs`" + ` defaults to 0 (all lines)
- Short flag ` + "`-n`" + ` can be used instead of ` + "`--lines`" + ` for log commands
- ` + "`completion`" + ` command supports shells: ` + "`bash`" + `, ` + "`zsh`" + `, ` + "`fish`" + `, ` + "`powershell`" + `
- Resource statuses: ` + "`running`" + `, ` + "`stopped`" + `, ` + "`error`" + `
- Teams use numeric IDs (not UUIDs) - this is the only resource that uses IDs
- Fields marked ` + "`sensitive:\"true\"`" + ` (tokens, passwords, IPs, emails) are hidden by default; use ` + "`--show-sensitive`" + ` to reveal
---
## Command Reference
`
func buildQuickLLMSText(fullReferencePath string) string {
return fmt.Sprintf(llmsQuickTemplate, fullReferencePath)
}
func buildFullLLMSText(quickReferencePath string) string {
var sb strings.Builder
fmt.Fprintf(&sb, llmsFullIntro, quickReferencePath)
writeLLMsAliases(&sb, rootCmd, "coolify")
sb.WriteString(llmsFullBody)
writeLLMsCommand(&sb, rootCmd, "coolify")
return sb.String()
}
func writeLLMsArtifacts(outputFile, fullOutputFile string) error {
if filepath.Clean(outputFile) == filepath.Clean(fullOutputFile) {
return fmt.Errorf("output and full-output must be different files")
}
if err := ensureParentDir(outputFile); err != nil {
return err
}
if err := ensureParentDir(fullOutputFile); err != nil {
return err
}
quickReferencePath := llmsReferencePath(fullOutputFile, outputFile)
fullReferencePath := llmsReferencePath(outputFile, fullOutputFile)
if err := os.WriteFile(outputFile, []byte(buildQuickLLMSText(fullReferencePath)), 0600); err != nil {
return fmt.Errorf("failed to write llms.txt: %w", err)
}
if err := os.WriteFile(fullOutputFile, []byte(buildFullLLMSText(quickReferencePath)), 0600); err != nil {
return fmt.Errorf("failed to write llms-full.txt: %w", err)
}
absQuickPath, _ := filepath.Abs(outputFile)
absFullPath, _ := filepath.Abs(fullOutputFile)
fmt.Printf("llms.txt generated successfully: %s\n", absQuickPath)
fmt.Printf("llms-full.txt generated successfully: %s\n", absFullPath)
return nil
}
func ensureParentDir(path string) error {
parentDir := filepath.Dir(path)
if err := os.MkdirAll(parentDir, 0750); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
return nil
}
func llmsReferencePath(fromFile, toFile string) string {
referencePath, err := filepath.Rel(filepath.Dir(fromFile), toFile)
if err != nil {
return filepath.ToSlash(toFile)
}
referencePath = filepath.ToSlash(referencePath)
if strings.HasPrefix(referencePath, ".") || strings.HasPrefix(referencePath, "/") {
return referencePath
}
return "./" + referencePath
}
// writeLLMsAliases writes aliases derived from the Cobra command tree.
func writeLLMsAliases(sb *strings.Builder, cmd *cobra.Command, parentPath string) {
aliases := collectLLMsAliases(cmd, parentPath)
if len(aliases) == 0 {
return
}
sb.WriteString("\n## Command Aliases\n\n")
sb.WriteString("Aliases are derived from the CLI command tree:\n")
for _, aliasLine := range aliases {
fmt.Fprintf(sb, "- %s\n", aliasLine)
}
}
func collectLLMsAliases(cmd *cobra.Command, parentPath string) []string {
var aliases []string
if cmd.Name() != "docs" && cmd.Name() != "help" {
if len(cmd.Aliases) > 0 {
aliasNames := append([]string{cmd.Name()}, cmd.Aliases...)
for i := range aliasNames {
aliasNames[i] = fmt.Sprintf("`%s`", commandPathPrefix(parentPath, cmd)+aliasNames[i])
}
aliases = append(aliases, strings.Join(aliasNames, " | "))
}
}
for _, child := range cmd.Commands() {
if child.Hidden || child.Name() == "help" {
continue
}
aliases = append(aliases, collectLLMsAliases(child, llmsCommandName(parentPath, cmd))...)
}
slices.Sort(aliases)
return slices.Compact(aliases)
}
func llmsCommandName(parentPath string, cmd *cobra.Command) string {
if !cmd.HasParent() {
return parentPath
}
parts := strings.Fields(cmd.Use)
commandPath := parentPath + " " + parts[0]
if len(parts) > 1 {
commandPath += " " + strings.Join(parts[1:], " ")
}
return commandPath
}
func commandPathPrefix(parentPath string, cmd *cobra.Command) string {
if cmd.HasParent() {
return parentPath + " "
}
return ""
}
// writeLLMsCommand recursively writes command documentation in llms.txt format.
func writeLLMsCommand(sb *strings.Builder, cmd *cobra.Command, parentPath string) {
// Build the full command path including args from Use field
commandPath := llmsCommandName(parentPath, cmd)
// Skip the docs command itself and help command
if cmd.Name() == "docs" || cmd.Name() == "help" {
return
}
// Determine if this command should be written
isRoot := !cmd.HasParent()
isRunnable := cmd.RunE != nil || cmd.Run != nil
hasVisibleChildren := false
for _, child := range cmd.Commands() {
if !child.Hidden && child.Name() != "help" {
hasVisibleChildren = true
break
}
}
// Write the root command, runnable commands, and leaf commands (no children)
if isRoot || isRunnable || !hasVisibleChildren {
// Get description - prefer Long if it's a single clean sentence, otherwise use Short
description := cmd.Short
if cmd.Long != "" {
longLines := strings.Split(strings.TrimSpace(cmd.Long), "\n")
if len(longLines) == 1 && len(longLines[0]) < 200 {
description = longLines[0]
}
}
fmt.Fprintf(sb, "Command: %s\n", commandPath)
fmt.Fprintf(sb, "Description: %s\n", description)
// For root command, show persistent flags; for others, show local flags
var flags []*pflag.Flag
if isRoot {
cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) {
if f.Name == "help" {
return
}
flags = append(flags, f)
})
} else {
cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
if f.Name == "help" {
return
}
flags = append(flags, f)
})
}
if len(flags) == 0 {
sb.WriteString("Parameters: (None)\n")
} else {
sb.WriteString("Parameters:\n")
for _, f := range flags {
flagType := f.Value.Type()
// Normalize type names
switch flagType {
case "int", "int32", "int64":
flagType = "integer"
case "bool":
flagType = "boolean"
}
// Check if the flag is marked as required via cobra annotation
// or via "(required)" in the usage string
required := isFlagRequired(f)
if f.Shorthand != "" {
fmt.Fprintf(sb, " - name: --%s (-%s)\n", f.Name, f.Shorthand)
} else {
fmt.Fprintf(sb, " - name: --%s\n", f.Name)
}
fmt.Fprintf(sb, " type: %s\n", flagType)
fmt.Fprintf(sb, " description: %s\n", f.Usage)
fmt.Fprintf(sb, " required: %t\n", required)
if f.DefValue != "" && f.DefValue != "[]" {
fmt.Fprintf(sb, " default: %s\n", f.DefValue)
}
}
}
sb.WriteString("\n")
}
// Recurse into subcommands
for _, child := range cmd.Commands() {
if child.Hidden || child.Name() == "help" {
continue
}
childPath := parentPath
if cmd.HasParent() {
childPath = llmsCommandName(parentPath, cmd)
}
writeLLMsCommand(sb, child, childPath)
}
}
// isFlagRequired checks if a flag is required by looking at cobra annotations
// and the "(required)" convention in usage strings.
func isFlagRequired(f *pflag.Flag) bool {
// Check cobra's MarkFlagRequired annotation
if ann, ok := f.Annotations[cobra.BashCompOneRequiredFlag]; ok && len(ann) > 0 && ann[0] == "true" {
return true
}
// Check for "(required)" in usage string (convention used in this codebase)
return strings.Contains(strings.ToLower(f.Usage), "(required)")
}
func NewDocsCommand() *cobra.Command {
docsCmd.AddCommand(manCmd)
docsCmd.AddCommand(markdownCmd)
docsCmd.AddCommand(llmsCmd)
manCmd.Flags().StringP("output-dir", "o", "./man", "Output directory for man pages")
markdownCmd.Flags().StringP("output-dir", "o", "./docs", "Output directory for markdown files")
llmsCmd.Flags().StringP("output", "o", "./llms.txt", "Output file path")
llmsCmd.Flags().String("full-output", "./llms-full.txt", "Full output file path")
return docsCmd
}
+118
View File
@@ -0,0 +1,118 @@
package cmd
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/spf13/cobra"
)
func TestWriteLLMsCommandIncludesShorthandAndDefaults(t *testing.T) {
root := &cobra.Command{Use: "coolify"}
child := &cobra.Command{
Use: "logs <uuid>",
Short: "Show logs",
Run: func(_ *cobra.Command, _ []string) {},
}
child.Flags().IntP("lines", "n", 0, "Number of log lines to display (0 = all)")
child.Flags().Bool("verbose", false, "Verbose output")
child.Flags().Bool("enabled", true, "Enabled by default")
root.AddCommand(child)
var sb strings.Builder
writeLLMsCommand(&sb, child, "coolify")
got := sb.String()
for _, want := range []string{
"Command: coolify logs <uuid>",
" - name: --lines (-n)",
" default: 0",
" - name: --verbose",
" default: false",
" - name: --enabled",
" default: true",
} {
if !strings.Contains(got, want) {
t.Fatalf("expected output to contain %q\nfull output:\n%s", want, got)
}
}
}
func TestWriteLLMsAliasesUsesCommandTree(t *testing.T) {
root := &cobra.Command{Use: "coolify"}
teams := &cobra.Command{Use: "teams", Aliases: []string{"team"}}
members := &cobra.Command{Use: "members", Aliases: []string{"member"}}
start := &cobra.Command{
Use: "start <uuid>",
Aliases: []string{"deploy"},
}
root.AddCommand(teams)
root.AddCommand(start)
teams.AddCommand(members)
var sb strings.Builder
writeLLMsAliases(&sb, root, "coolify")
got := sb.String()
for _, want := range []string{
"## Command Aliases",
"`coolify start` | `coolify deploy`",
"`coolify teams` | `coolify team`",
"`coolify teams members` | `coolify teams member`",
} {
if !strings.Contains(got, want) {
t.Fatalf("expected alias output to contain %q\nfull output:\n%s", want, got)
}
}
}
func TestBuildQuickLLMSTextIncludesCoreGuidance(t *testing.T) {
got := buildQuickLLMSText("./llms-full.txt")
for _, want := range []string{
"# Coolify CLI - llms.txt",
"Prefer `--format json` for automation and parsing.",
"coolify context verify",
"coolify app logs <uuid> --follow",
"Full command and parameter catalog: ./llms-full.txt",
} {
if !strings.Contains(got, want) {
t.Fatalf("expected quick llms output to contain %q\nfull output:\n%s", want, got)
}
}
}
func TestWriteLLMsArtifactsWritesQuickAndFullFiles(t *testing.T) {
tempDir := t.TempDir()
quickPath := filepath.Join(tempDir, "llms.txt")
fullPath := filepath.Join(tempDir, "nested", "llms-full.txt")
if err := writeLLMsArtifacts(quickPath, fullPath); err != nil {
t.Fatalf("writeLLMsArtifacts() error = %v", err)
}
quickContent, err := os.ReadFile(quickPath)
if err != nil {
t.Fatalf("failed reading quick file: %v", err)
}
fullContent, err := os.ReadFile(fullPath)
if err != nil {
t.Fatalf("failed reading full file: %v", err)
}
for _, want := range []struct {
content string
substr string
}{
{string(quickContent), "./nested/llms-full.txt"},
{string(fullContent), "../llms.txt"},
{string(fullContent), "## Command Reference"},
} {
if !strings.Contains(want.content, want.substr) {
t.Fatalf("expected generated content to contain %q", want.substr)
}
}
}
+3 -3
View File
@@ -1,13 +1,13 @@
package github
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewListBranchesCommand() *cobra.Command {
@@ -19,7 +19,7 @@ func NewListBranchesCommand() *cobra.Command {
Example: coolify github branches abc-123-def owner/repository`,
Args: cli.ExactArgs(2, "<app_uuid> <owner/repo>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
appUUID := args[0]
// Parse owner/repo
+12 -12
View File
@@ -1,14 +1,14 @@
package github
import (
"context"
"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"
"github.com/spf13/cobra"
)
func NewCreateCommand() *cobra.Command {
@@ -20,8 +20,8 @@ func NewCreateCommand() *cobra.Command {
Required flags: --name, --api-url, --html-url, --app-id, --installation-id, --client-id, --client-secret, --private-key-uuid
Example: coolify github create --name "My GitHub App" --api-url "https://api.github.com" --html-url "https://github.com" --app-id 123456 --installation-id 789012 --client-id "Iv1.abc123" --client-secret "secret123" --private-key-uuid "abc-123-def-456"`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
@@ -100,14 +100,14 @@ Example: coolify github create --name "My GitHub App" --api-url "https://api.git
createCmd.Flags().String("private-key-uuid", "", "UUID of existing private key (required)")
createCmd.Flags().Bool("system-wide", false, "Is this app system-wide (cloud only)")
createCmd.MarkFlagRequired("name")
createCmd.MarkFlagRequired("api-url")
createCmd.MarkFlagRequired("html-url")
createCmd.MarkFlagRequired("app-id")
createCmd.MarkFlagRequired("installation-id")
createCmd.MarkFlagRequired("client-id")
createCmd.MarkFlagRequired("client-secret")
createCmd.MarkFlagRequired("private-key-uuid")
_ = createCmd.MarkFlagRequired("name")
_ = createCmd.MarkFlagRequired("api-url")
_ = createCmd.MarkFlagRequired("html-url")
_ = createCmd.MarkFlagRequired("app-id")
_ = createCmd.MarkFlagRequired("installation-id")
_ = createCmd.MarkFlagRequired("client-id")
_ = createCmd.MarkFlagRequired("client-secret")
_ = createCmd.MarkFlagRequired("private-key-uuid")
return createCmd
}
+8 -4
View File
@@ -1,12 +1,12 @@
package github
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewDeleteCommand() *cobra.Command {
@@ -16,7 +16,7 @@ func NewDeleteCommand() *cobra.Command {
Long: `Delete a GitHub App integration. The app must not be used by any applications.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
@@ -30,7 +30,11 @@ func NewDeleteCommand() *cobra.Command {
if !force {
var response string
fmt.Printf("Are you sure you want to delete GitHub App %s? This cannot be undone. (yes/no): ", appUUID)
fmt.Scanln(&response)
_, err := fmt.Scanln(&response)
if err != nil {
return fmt.Errorf("failed to read confirmation: %w", err)
}
if response != "yes" && response != "y" {
fmt.Println("Delete cancelled.")
+3 -3
View File
@@ -1,13 +1,13 @@
package github
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewGetCommand() *cobra.Command {
@@ -17,7 +17,7 @@ func NewGetCommand() *cobra.Command {
Long: `Get detailed information about a specific GitHub App integration.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
+9 -4
View File
@@ -1,13 +1,13 @@
package github
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewListCommand() *cobra.Command {
@@ -15,14 +15,19 @@ func NewListCommand() *cobra.Command {
Use: "list",
Short: "List all GitHub App integrations",
Long: `List all GitHub App integrations configured in Coolify.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Check minimum version requirement
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.436"); err != nil {
return err
}
svc := service.NewGitHubAppService(client)
apps, err := svc.List(ctx)
if err != nil {
+3 -3
View File
@@ -1,13 +1,13 @@
package github
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
func NewListRepositoriesCommand() *cobra.Command {
@@ -17,7 +17,7 @@ func NewListRepositoriesCommand() *cobra.Command {
Long: `List all repositories that are accessible by the specified GitHub App.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
+3 -3
View File
@@ -1,13 +1,13 @@
package github
import (
"context"
"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/service"
"github.com/spf13/cobra"
)
func NewUpdateCommand() *cobra.Command {
@@ -17,7 +17,7 @@ func NewUpdateCommand() *cobra.Command {
Long: `Update an existing GitHub App integration. Provide the app UUID and the fields you want to update.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
+3 -3
View File
@@ -1,14 +1,14 @@
package privatekeys
import (
"context"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewCreateCommand creates the create command
@@ -19,7 +19,7 @@ func NewCreateCommand() *cobra.Command {
Args: cli.ExactArgs(2, "<key_name> <private_key_or_file>"),
Short: "Add a private key",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
name := args[0]
privateKeyInput := args[1]
+3 -3
View File
@@ -1,12 +1,12 @@
package privatekeys
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewDeleteCommand creates the delete command
@@ -16,7 +16,7 @@ func NewDeleteCommand() *cobra.Command {
Args: cli.ExactArgs(1, "<uuid>"),
Short: "Remove a private key",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
+4 -4
View File
@@ -1,13 +1,13 @@
package privatekeys
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// NewListCommand creates the list command
@@ -15,8 +15,8 @@ func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all private keys",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
+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
}
+4 -4
View File
@@ -1,13 +1,13 @@
package project
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
"github.com/spf13/cobra"
)
// EnvironmentRow represents an environment for display
@@ -23,7 +23,7 @@ func NewGetCommand() *cobra.Command {
Short: "Get a project by uuid",
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
@@ -55,7 +55,7 @@ func NewGetCommand() *cobra.Command {
var rows []EnvironmentRow
// If the project has environments, expand them
if project.Environments != nil && len(project.Environments) > 0 {
if len(project.Environments) > 0 {
for _, env := range project.Environments {
rows = append(rows, EnvironmentRow{
UUID: env.UUID,

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