Compare commits

...

118 Commits

Author SHA1 Message Date
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
Andras Bacsai 6cad3ba0f7 Merge pull request #23 from coollabsio/cli-ux-improvements
Refactor: Use 'context' instead of 'instance' terminology
2025-10-17 10:46:51 +02:00
Andras Bacsai 1bc4625ef8 fix: properly hide sensitive data in all commands
- Add sensitive:"true" tag to Instance.Token field
- Add sensitive:"true" tag to GitHub ClientSecret and WebhookSecret
- Add sensitive:"true" tag to EnvironmentVariable Value and RealValue
- Tokens now hidden by default, shown with --show-sensitive flag

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

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

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 09:19:11 +02:00
Andras Bacsai c98a8330f8 Changes auto-committed by Conductor 2025-10-17 09:13:39 +02:00
Andras Bacsai 49e5870c86 Merge pull request #22 from YaRissi/fix/missing-flags
Fix: missing flags for some commands
2025-10-17 09:13:16 +02:00
YaRissi 705fed3560 fix: update README to reflect context management commands and terminology 2025-10-17 01:21:52 +02:00
YaRissi 88d0b25537 fix: correct flag names for local backup retention in create command and update README 2025-10-17 01:10:28 +02:00
YaRissi 3470b04235 fix: add missing flags to commands 2025-10-17 00:54:05 +02:00
Andras Bacsai eb876e4bda Merge pull request #21 from YaRissi/folder-file-structure
Proposal: Restructure of commands
2025-10-17 00:19:02 +02:00
Andras Bacsai 7210b8df76 refactor: Rename instances to contexts and add update command 2025-10-17 00:09:48 +02:00
YaRissi 0546fd1932 fix: autocompletion docs 2025-10-16 23:22:28 +02:00
YaRissi e2c1c86194 fix: team models 2025-10-16 23:08:16 +02:00
YaRissi 84e7e4921b fix: move command to get server domains by UUID and remove non existing domain endpoint 2025-10-16 22:55:53 +02:00
YaRissi 0acb1fc512 fix: docs command 2025-10-16 22:34:20 +02:00
YaRissi eaf9614bcc Refactor CLI commands files
- Introduced new structured command files for teams: current, get, list, and members.
- Created a new CLI client helper for API interactions.
- Removed deprecated version command and replaced it with a new structure.
- Moved utility functions from root to cli folder in internal
2025-10-16 22:29:02 +02:00
Andras Bacsai 8ec750ecc6 Changes auto-committed by Conductor (#19) 2025-10-16 17:02:52 +02:00
Andras Bacsai 76396c3c06 Merge pull request #18 from coollabsio/andrasbacsai/review-install-script
Enhance install script with features and error handling
2025-10-16 14:56:58 +02:00
Andras Bacsai 3286229a06 Changes auto-committed by Conductor 2025-10-16 14:42:15 +02:00
Andras Bacsai 884b687947 Merge pull request #17 from coollabsio/andrasbacsai/api-endpoint-expansion
update README.md
2025-10-16 14:09:57 +02:00
Andras Bacsai 11f9baafc6 feat: expand CLI documentation with application, database, and service management commands 2025-10-16 14:09:20 +02:00
Andras Bacsai f4f628fdae Merge pull request #16 from coollabsio/andrasbacsai/api-endpoint-expansion
Refactor cli and use all available endpoints
2025-10-16 14:01:53 +02:00
Andras Bacsai ae086bbbbd Changes auto-committed by Conductor 2025-10-16 13:49:17 +02:00
Andras Bacsai 401f4bf317 Changes auto-committed by Conductor 2025-10-16 13:48:57 +02:00
Andras Bacsai 095b5a5bc5 Merge pull request #14 from coollabsio/add-testing-framework
docs: update CLAUDE.md with correct test commands
2025-10-14 23:26:08 +02:00
Andras Bacsai 002e206c54 feat: add air file watcher for hot reload during development
- Add .air.toml configuration for automatic rebuilds
- Update conductor.json run script to use air
- Update conductor-setup.sh to install air automatically
- Air watches .go files and rebuilds coolify binary on changes
- Excludes test files, vendor, and conductor directories from watch

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 23:24:20 +02:00
Andras Bacsai c865aa7512 docs: update CLAUDE.md with correct test commands
- Update test commands to use ./internal/... path
- Add note about httptest.NewServer() for API mocking
- Clarify that tests never call real external APIs
- Update checklist to reflect internal/ test structure

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 23:16:22 +02:00
Andras Bacsai 3788d6b812 Merge pull request #13 from coollabsio/create-script
feat: add Conductor workspace configuration
2025-10-14 23:01:31 +02:00
Andras Bacsai 7d23eac444 feat: add Conductor workspace configuration
Add conductor.json and setup script to enable automated workspace setup:
- Auto-installs Go dependencies
- Validates Go version (1.24+)
- Builds coolify binary on workspace creation
- Configures test runner for development

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 23:00:59 +02:00
Andras Bacsai 6aa77a4840 Merge pull request #12 from coollabsio/update-go-deps
chore: update Go dependencies to fix security vulnerabilities
2025-10-14 22:55:39 +02:00
Andras Bacsai 17f1435ce1 chore: update Go dependencies to fix security vulnerabilities
Updated key dependencies to address security vulnerabilities:
- golang.org/x/oauth2: v0.25.0 → v0.32.0 (High severity)
- golang.org/x/crypto: v0.32.0 → v0.43.0 (High severity DoS)
- github.com/ulikunitz/xz: v0.5.12 → v0.5.15 (Moderate memory leak)

Also updated:
- Go version: 1.22.0 → 1.24.6
- github.com/spf13/cobra: v1.8.1 → v1.10.1
- github.com/spf13/viper: v1.19.0 → v1.21.0
- 20+ other dependency updates

All tests passing.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 22:54:47 +02:00
Andras Bacsai 485b4c5f6b Merge pull request #9 from jizaymes/fix-update-text
fix cmd typo prompting user to update coolify
2025-10-14 22:50:15 +02:00
Andras Bacsai d930e6cf9f Merge pull request #11 from coollabsio/andrasbacsai/cli-restructure
feat: Complete CLI restructure with layered architecture
2025-10-14 22:49:15 +02:00
Andras Bacsai 519fe69ebf feat: complete CLI restructure with layered architecture
This commit completes a comprehensive restructuring of the Coolify CLI, implementing a clean layered architecture with improved maintainability, testability, and user experience.

## Major Changes

### Architecture Refactoring (Phases 1-3)
- Separated concerns into distinct layers: Commands → Services → API Client
- Created `internal/api/` - HTTP client with retry logic, error handling
- Created `internal/service/` - Business logic layer for all operations
- Created `internal/config/` - Multi-instance configuration management
- Created `internal/models/` - Shared data structures
- Created `internal/output/` - Pluggable formatters (table/json/pretty)

### New Features (Phase 4)
- **Named deployments**: `coolify deploy name <name>` - Deploy resources by name
- **Batch deployments**: `coolify deploy batch <name1,name2,...>` - Deploy multiple resources
- **Instance selection**: `--instance <name>` flag to use specific instances
- **Improved error messages**: Clean, actionable error output without usage clutter

### Testing & Documentation (Phase 5)
- **Test coverage**: 80%+ across all internal packages
- **Man pages**: Professional documentation (39 man pages)
- **Architecture docs**: Comprehensive ARCHITECTURE.md with diagrams
- **Practical examples**: Real-world scripts for CI/CD, multi-env deployments
- **Complete Godoc**: All public functions documented

## Benefits

### For Users
- More intuitive commands with consistent patterns
- Better error messages and debugging
- Multi-instance support for prod/staging workflows
- Professional documentation (man pages, examples)

### For Developers
- Clear separation of concerns
- Comprehensive test suite (80%+ coverage)
- Easy to add new commands/features
- Well-documented architecture

## Commands Enhanced

- `servers` - Refactored to use service layer
- `projects` - Improved with service pattern
- `deploy` - Added name-based and batch deployments
- `resources` - Refactored with cleaner code
- `domains` - Updated to new architecture
- `private-keys` - Improved with service layer
- `instances` - Enhanced configuration management

## New Commands

- `coolify deploy name <name>` - Deploy by resource name
- `coolify deploy batch <names>` - Deploy multiple resources
- `coolify docs man` - Generate man pages
- `coolify docs markdown` - Generate markdown documentation

## Files Added

### Core Architecture
- `internal/api/` - API client layer (4 files)
- `internal/service/` - Service layer (12 files)
- `internal/config/` - Configuration (4 files)
- `internal/models/` - Data models (8 files)
- `internal/output/` - Output formatters (5 files)

### Documentation
- `ARCHITECTURE.md` - Comprehensive architecture guide
- `CLAUDE.md` - Development instructions
- `examples/` - Practical usage examples

### Testing
- `test/fixtures/` - Test data
- `*_test.go` - Comprehensive test suites (80%+ coverage)

## Breaking Changes

None - All existing commands remain backward compatible.

## Migration Guide

No migration needed. All existing workflows continue to work.

New features are opt-in:
- Use `--instance` flag for multi-instance setups
- Use `deploy name` for name-based deployments
- Use `deploy batch` for multiple deployments

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 22:29:11 +02:00
James Cornman 4e0575a156 update text prompting user to update coolify 2025-05-20 11:00:09 -05:00
184 changed files with 18622 additions and 1697 deletions
+46
View File
@@ -0,0 +1,46 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./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/coolify'"
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = true
keep_scroll = true
+7
View File
@@ -0,0 +1,7 @@
## Changes
-
## Issues & Discussions
- fix #
+34 -5
View File
@@ -10,18 +10,47 @@ 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 }}
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
+55
View File
@@ -0,0 +1,55 @@
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 ./...
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."
+11 -3
View File
@@ -1,4 +1,12 @@
coolify-cli
coolify
cli
config.json
/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
+27 -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,18 @@ builds:
- amd64
- arm64
env:
- CGO_ENABLED=0
- CGO_ENABLED=0
checksum:
name_template: checksums.txt
algorithm: sha256
archives:
- id: coolify-archive
ids:
- coolify
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
format_overrides:
- goos: windows
formats: [zip]
+587
View File
@@ -0,0 +1,587 @@
# Coolify CLI Architecture
This document describes the architecture and design principles of the Coolify CLI.
## Overview
The Coolify CLI is a command-line interface for managing Coolify instances, servers, projects, and deployments. It follows a layered architecture pattern that separates concerns and promotes maintainability.
## Architecture Layers
```
┌─────────────────────────────────────────────────────────┐
│ User Interface │
│ (Terminal/Shell) │
└────────────────────────┬────────────────────────────────┘
┌────────────────────────▼────────────────────────────────┐
│ Command Layer (cmd/) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ servers │ │ deploy │ │ projects │ ... │
│ └──────────┘ └──────────┘ └──────────┘ │
│ • CLI parsing & validation │
│ • Flag handling │
│ • Output formatting │
└────────────────────────┬────────────────────────────────┘
┌────────────────────────▼────────────────────────────────┐
│ Service Layer (internal/service/) │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ServerService│ │DeployService │ │ProjectService│ │
│ └─────────────┘ └──────────────┘ └──────────────┘ │
│ • Business logic │
│ • Request validation │
│ • Response transformation │
└────────────────────────┬────────────────────────────────┘
┌────────────────────────▼────────────────────────────────┐
│ API Client Layer (internal/api/) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ HTTP Client (api.Client) │ │
│ └───────────────────────────────────────────────────┘ │
│ • HTTP requests/responses │
│ • Authentication (Bearer tokens) │
│ • Retry logic │
│ • Error handling │
└────────────────────────┬────────────────────────────────┘
┌────────────────────────▼────────────────────────────────┐
│ Coolify API (External) │
│ https://instance.coolify.io/api/v1/ │
└─────────────────────────────────────────────────────────┘
```
## Supporting Components
```
┌─────────────────────────────────────────────────────────┐
│ Configuration (internal/config/) │
│ • Multi-instance management │
│ • Default instance selection │
│ • Token storage │
│ • ~/.config/coolify/config.json │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Output Formatters (internal/output/) │
│ ┌─────────┐ ┌────────┐ ┌─────────┐ │
│ │ Table │ │ JSON │ │ Pretty │ │
│ └─────────┘ └────────┘ └─────────┘ │
│ • Flexible output formats │
│ • Sensitive data masking │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Data Models (internal/models/) │
│ • Server, Project, Resource, Deployment │
│ • Request/Response structures │
│ • JSON marshaling/unmarshaling │
└─────────────────────────────────────────────────────────┘
```
## Layer Responsibilities
### 1. Command Layer (`cmd/`)
**Purpose**: Handle CLI user interface and interaction
**Responsibilities**:
- Parse command-line arguments and flags
- Validate user input
- Coordinate with service layer
- Format and display output
- Handle errors gracefully
**Key Files**:
- `root.go` - Root command, global flags, initialization
- `servers.go` - Server management commands
- `deploy.go` - Deployment commands
- `context.go` - Context (instance) configuration commands
- `projects.go` - Project listing and inspection
- etc.
**Example**:
```go
var serversListCmd = &cobra.Command{
Use: "list",
Short: "List all servers",
RunE: func(cmd *cobra.Command, args []string) error {
// Get API client
client, err := getAPIClient(cmd)
if err != nil {
return err
}
// Use service layer
service := service.NewServerService(client)
servers, err := service.List(cmd.Context())
if err != nil {
return err
}
// Format and display output
formatter, _ := getFormatter(cmd)
return formatter.Format(servers)
},
}
```
### 2. Service Layer (`internal/service/`)
**Purpose**: Implement business logic and coordinate API calls
**Responsibilities**:
- Validate business rules
- Coordinate multiple API calls if needed
- Transform API responses to CLI-friendly format
- Handle service-specific error cases
**Key Files**:
- `server.go` - Server operations
- `deployment.go` - Deployment operations
- `project.go` - Project operations
- `resource.go` - Resource operations
- `privatekey.go` - SSH key operations
- `domain.go` - Domain operations
**Example**:
```go
type ServerService struct {
client *api.Client
}
func (s *ServerService) List(ctx context.Context) ([]models.Server, error) {
var servers []models.Server
err := s.client.Get(ctx, "servers", &servers)
return servers, err
}
```
### 3. API Client Layer (`internal/api/`)
**Purpose**: Handle all HTTP communication with Coolify API
**Responsibilities**:
- Construct HTTP requests
- Add authentication headers
- Retry failed requests with exponential backoff
- Parse HTTP responses
- Convert HTTP errors to meaningful error messages
**Key Files**:
- `client.go` - HTTP client implementation
- `error.go` - API error handling
- `options.go` - Client configuration options
**Example**:
```go
type Client struct {
baseURL string
token string
httpClient *http.Client
retries int
timeout time.Duration
}
func (c *Client) Get(ctx context.Context, path string, result interface{}) error {
return c.doRequest(ctx, "GET", path, nil, result)
}
```
### 4. Configuration Layer (`internal/config/`)
**Purpose**: Manage CLI configuration and multiple instances
**Responsibilities**:
- Load/save configuration from disk
- Manage multiple Coolify instances
- Select default instance
- Store API tokens securely (file permissions)
**Key Files**:
- `config.go` - Configuration structure and methods
- `instance.go` - Instance definition
- `loader.go` - File I/O operations
**Configuration File** (`~/.config/coolify/config.json`):
```json
{
"instances": [
{
"name": "prod",
"fqdn": "https://coolify.example.com",
"token": "your-api-token",
"default": true
},
{
"name": "staging",
"fqdn": "https://staging.coolify.example.com",
"token": "staging-token"
}
]
}
```
### 5. Output Layer (`internal/output/`)
**Purpose**: Format data for display to users
**Responsibilities**:
- Format data as tables, JSON, or pretty-printed JSON
- Hide sensitive information unless `--show-sensitive` is used
- Handle different data types (slices, structs, primitives)
**Key Files**:
- `formatter.go` - Formatter interface
- `table.go` - Table formatting
- `json.go` - JSON formatting
- `pretty.go` - Pretty JSON formatting
**Supported Formats**:
- `table` - Default, human-readable tables
- `json` - Compact JSON for scripting
- `pretty` - Indented JSON for debugging
### 6. Models Layer (`internal/models/`)
**Purpose**: Define data structures
**Responsibilities**:
- Define API request/response structures
- JSON tags for marshaling
- Common types and timestamps
**Key Files**:
- `server.go` - Server-related types
- `project.go` - Project-related types
- `resource.go` - Resource types
- `deployment.go` - Deployment types
- `common.go` - Shared types
## Data Flow
### Example: Listing Servers
1. **User Input**: `coolify servers list --format=table`
2. **Command Layer** (`cmd/servers.go`):
- Cobra parses the command
- `serversListCmd.RunE` is executed
- Gets API client using `getAPIClient()`
- Creates ServerService instance
3. **Service Layer** (`internal/service/server.go`):
- `ServerService.List()` is called
- Validates context (if needed)
- Calls API client
4. **API Client Layer** (`internal/api/client.go`):
- Constructs GET request to `/api/v1/servers`
- Adds Bearer token authentication
- Sends HTTP request
- Retries on failure (with backoff)
- Parses JSON response
5. **Response Processing**:
- JSON unmarshaled to `[]models.Server`
- Returns to service layer
- Returns to command layer
6. **Output Layer** (`internal/output/table.go`):
- Command layer creates table formatter
- Formatter processes server data
- Formats as table with columns
- Writes to stdout
7. **User Output**: Table displayed in terminal
## Design Patterns
### 1. Dependency Injection
Services receive the API client as a constructor parameter:
```go
func NewServerService(client *api.Client) *ServerService {
return &ServerService{client: client}
}
```
**Benefits**:
- Easy to test (can inject mock client)
- Clear dependencies
- Flexible configuration
### 2. Strategy Pattern (Output Formatters)
Different formatters implement the same interface:
```go
type Formatter interface {
Format(data interface{}) error
}
```
**Benefits**:
- Easy to add new formats
- Consistent API
- Runtime format selection
### 3. Options Pattern (API Client)
Client configuration uses functional options:
```go
client := api.NewClient(url, token,
api.WithDebug(true),
api.WithRetries(5),
api.WithTimeout(60 * time.Second),
)
```
**Benefits**:
- Optional parameters
- Clear intent
- Backward compatible
### 4. Error Wrapping
Errors are wrapped with context at each layer:
```go
if err != nil {
return fmt.Errorf("failed to list servers: %w", err)
}
```
**Benefits**:
- Error context preserved
- Stack trace maintained
- Better debugging
## Testing Strategy
### Unit Tests
Each layer has comprehensive unit tests:
- **Commands**: Mock services, test flag parsing
- **Services**: Mock API client, test business logic
- **API Client**: Use `httptest.Server`, test HTTP handling
- **Config**: Test file I/O with temp directories
- **Output**: Test formatting with buffers
### Integration Tests
Test multiple layers together:
- Commands + Services + Mock API
- Config + File System
- End-to-end workflows
### Coverage Goals
- Overall: 70%+
- New features: 80%+
- Critical paths: 90%+
## Configuration Files
### CLI Configuration
**Location**: `~/.config/coolify/config.json` (Linux/macOS)
**Location**: `%APPDATA%\coolify\config.json` (Windows)
**Structure**:
```json
{
"instances": [
{
"name": "prod",
"fqdn": "https://coolify.example.com",
"token": "your-token",
"default": true
}
],
"lastUpdateCheckTime": "2025-01-15T10:30:00Z"
}
```
## API Communication
### Base URL
All API calls use: `{fqdn}/api/v1/{endpoint}`
Example: `https://coolify.example.com/api/v1/servers`
### Authentication
Bearer token authentication:
```
Authorization: Bearer {token}
```
### Request/Response
**Content-Type**: `application/json`
**Request Body** (POST):
```json
{
"name": "my-server",
"ip": "192.168.1.100"
}
```
**Response Body**:
```json
{
"uuid": "abc123",
"name": "my-server",
"ip": "192.168.1.100"
}
```
### Error Handling
HTTP errors are converted to CLI-friendly messages:
- `401` → "Unauthenticated. Check your API token."
- `404` → "Resource not found."
- `500` → "Server error. Please try again."
### Retry Logic
Failed requests are retried with exponential backoff:
- Attempt 1: Immediate
- Attempt 2: Wait 1s
- Attempt 3: Wait 2s
- Attempt 4: Wait 4s
Does not retry on 4xx errors (except 429 rate limit).
## Security Considerations
### API Token Storage
- Stored in config file with restricted permissions (0600)
- Never logged (even in debug mode)
- Masked in output by default (use `-s` to show)
### Sensitive Data Handling
- Tokens masked as `********` in output
- Use `--show-sensitive` flag to reveal
- Debug logs sanitize sensitive data
### HTTPS
- All API communication uses HTTPS
- Certificate validation enabled
## Performance Optimizations
### Concurrent Operations
Batch deployments run in parallel:
```go
// Deploy multiple resources concurrently
var wg sync.WaitGroup
for _, name := range names {
wg.Add(1)
go func(n string) {
defer wg.Done()
deployResource(n)
}(name)
}
wg.Wait()
```
### Connection Reuse
HTTP client reuses connections:
```go
c.httpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 90 * time.Second,
},
}
```
### Minimal Dependencies
- Use Go standard library when possible
- Only essential external dependencies
- Keep binary size small
## Extensibility
### Adding a New Command
1. Create `cmd/newfeature.go`
2. Define Cobra command
3. Create service if needed (`internal/service/newfeature.go`)
4. Add models if needed (`internal/models/newfeature.go`)
5. Register command in `init()`
6. Write tests
### Adding a New Output Format
1. Create `internal/output/newformat.go`
2. Implement `Formatter` interface
3. Add format constant
4. Update `NewFormatter()` switch
### Adding API Client Features
1. Add method to `internal/api/client.go`
2. Add tests in `internal/api/client_test.go`
3. Use in service layer
## Build & Release
### Build Process
```bash
# Local build
go build -o coolify ./coolify
# Install locally
go install ./coolify
# Multi-platform release
goreleaser release --clean
```
### Release Artifacts
- Linux: amd64, arm64
- macOS: amd64, arm64 (Apple Silicon)
- Windows: amd64
### Distribution
- GitHub Releases
- Install script: `scripts/install.sh`
- Package managers (planned)
## Future Enhancements
- [ ] Shell completion improvements
- [ ] Interactive mode
- [ ] Configuration wizard
- [ ] Plugin system
- [ ] Telemetry (opt-in)
- [ ] Cache layer for frequent queries
## References
- [Cobra Documentation](https://cobra.dev/)
- [Coolify API Specification](https://github.com/coollabsio/coolify/blob/v4.x/openapi.json)
- [Go Project Layout](https://github.com/golang-standards/project-layout)
+343
View File
@@ -0,0 +1,343 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a CLI tool for interacting with the Coolify API, built with Go using the Cobra framework. The CLI allows users to manage Coolify instances (both cloud and self-hosted), servers, projects, resources, deployments, domains, and private keys.
### 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
- **Base Path**: `/api/v1/`
- **Authentication**: Bearer token (API tokens from Coolify dashboard at `/security/api-tokens`)
All commands in this CLI are wrappers around API endpoints defined in the OpenAPI specification. When adding new features or endpoints:
1. Check the OpenAPI spec for available endpoints and their request/response schemas
2. Ensure the CLI command structure follows the API resource hierarchy
3. Match the API's data types and validation rules
## Architecture
### Command Structure
The codebase follows Cobra's command pattern with a root command and subcommands:
- 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)
- `servers.go` - list and get server information
- `projects.go` - list projects with environments and applications
- `resources.go` - list resources
- `deploy.go` - deploy resources
- `domains.go` - manage domains
- `privatekeys.go` - manage SSH keys
- `update.go` - self-update CLI
- `version.go` - show CLI version
### Configuration Management
- Uses Viper for configuration management
- Config file location: `~/.config/coolify/config.json` (via xdg package)
- Config stores multiple instances with tokens, default instance selection
- Global flags available: `--token`, `--host`, `--format`, `--show-sensitive`, `--force`, `--debug`
### API Communication
Core API functions in `cmd/root.go`:
- `Fetch(url string)` - GET requests
- `Post(url, input)` - POST requests
- `Delete(url)` - DELETE requests
All API calls use `Fqdn + "/api/v1/" + url` pattern with Bearer token authentication
### Version Management
- CLI version tracking with auto-update check (10 minute interval)
- API version checking and minimum version enforcement via `CheckMinimumVersion()`
- Self-update capability using `go-selfupdate` library
### Output Formatting
Three output modes supported via `--format` flag:
- `table` (default) - tabwriter formatted output
- `json` - compact JSON
- `pretty` - indented JSON
## Development Commands
### Build
```bash
go build -o coolify ./coolify
```
### Run locally
```bash
go run ./coolify [command]
```
### Test a command
```bash
go run ./coolify context list
go run ./coolify servers list --debug
```
### Install locally
```bash
go install ./coolify
```
### Run tests
```bash
# Run all tests (tests are in internal/ directory)
go test ./internal/...
# Run with coverage
go test ./internal/... -cover
# Run with verbose output
go test ./internal/... -v
# Run specific package
go test ./internal/api/... -v
go test ./internal/service/... -v
# Run specific test
go test ./internal/api -run TestClient_Get_Success -v
```
### Before committing
```bash
# 1. Run tests
go test ./internal/...
# 2. Check coverage
go test ./internal/... -cover
# 3. Run linter (if available)
golangci-lint run
# 4. Format code
go fmt ./...
```
## Release Process
- Uses GoReleaser for multi-platform builds (Linux, Darwin, Windows on amd64/arm64)
- Release workflow: `.github/workflows/release-cli.yml` triggers on GitHub releases
- GoReleaser config: `.goreleaser.yml`
- Install script: `scripts/install.sh` downloads from GitHub releases
## Key Patterns
### Adding a New Command
1. Create new file in `cmd/` (e.g., `cmd/newfeature.go`)
2. Define command struct with cobra.Command
3. Implement Run function with:
- Call `CheckDefaultThings(nil)` to validate version and format
- Use `Fetch()`, `Post()`, or `Delete()` helpers
- Handle JSON unmarshaling into typed structs
- Support all three output formats
4. Register command in `init()` function: `rootCmd.AddCommand(yourCmd)`
### API Version Requirements
If a command requires a specific Coolify API version, pass it to `CheckDefaultThings()`:
```go
minimumVersion := "4.0.0"
CheckDefaultThings(&minimumVersion)
```
### Handling Sensitive Data
- Use `ShowSensitive` flag to control display of tokens/secrets
- Default overlay: `SensitiveInformationOverlay = "********"`
### UUID vs ID Pattern
**CRITICAL: Always use UUIDs for user-facing interactions, never internal database IDs.**
When adding new commands or models:
1. **Command Arguments**: Always accept UUIDs as string arguments (e.g., `<resource_uuid>`), never integer IDs
2. **API Endpoints**: Construct API paths using UUIDs (e.g., `resources/{uuid}`), not IDs
3. **Service Layer**: Methods should accept `uuid string` parameters, not `id int`
4. **Table Output**: Hide internal IDs from table output using `table:"-"` struct tags
5. **Model Fields**:
- Keep `ID int` field with `json:"id" table:"-"` (for API responses, hidden from users)
- Always include `UUID string` field with `json:"uuid"` (visible to users)
**Example model:**
```go
type Resource struct {
ID int `json:"id" table:"-"` // Hidden from table output
UUID string `json:"uuid"` // Shown in table output
Name string `json:"name"`
// ... other fields
}
```
**Why UUIDs?**
- UUIDs are stable across environments (dev, staging, prod)
- IDs are internal implementation details that can change
- UUIDs are more secure (don't expose database sequencing)
- Coolify API uses UUIDs as the primary resource identifier
## Testing Requirements
**CRITICAL: All code changes MUST include tests. This is non-negotiable.**
### Test Coverage Requirements
- **Minimum coverage**: 70% for all packages
- **New features**: Must have 80%+ coverage
- **Bug fixes**: Must include regression tests
- **Refactoring**: Must maintain or improve existing coverage
### Testing Structure
```
test/
├── fixtures/ # Test data, mock API responses
├── mocks/ # Mock implementations of interfaces
└── integration/ # Integration tests with test server
```
### Test Requirements by Package Type
#### 1. Command Tests (`cmd/*_test.go`)
- Test command parsing and flag handling
- Test output formatting (table, json, pretty)
- Use mock API client to avoid real API calls
- Test error handling and validation
- Example:
```go
func TestServersListCmd(t *testing.T) {
// Test with mock client
// Verify output format
// Test error cases
}
```
#### 2. API Client Tests (`internal/api/*_test.go`)
- Test request building
- Test response parsing
- Test error handling (4xx, 5xx status codes)
- Test retry logic
- Test timeout behavior
- **IMPORTANT**: Use `httptest.NewServer()` for mock HTTP responses (NOT real APIs)
- All API tests must use local mock servers, never call real Coolify cloud or external APIs
#### 3. Service Tests (`internal/service/*_test.go`)
- Test business logic
- Mock API client
- Test complex workflows
- Test error propagation
#### 4. Model Tests (`internal/models/*_test.go`)
- Test JSON marshaling/unmarshaling
- Test validation logic
- Test helper methods
#### 5. Integration Tests (`test/integration/*_test.go`)
- Test full command execution
- Test with real HTTP server (httptest)
- Test config file operations
- Test version checking
- Can be run with `-short` flag to skip
### Running Tests
```bash
# Run all tests (tests are in internal/ directory)
go test ./internal/...
# Run with coverage
go test ./internal/... -cover
# Generate coverage report
go test ./internal/... -coverprofile=coverage.out
go tool cover -html=coverage.out
# Run with verbose output
go test ./internal/... -v
# Run only unit tests (skip integration)
go test ./internal/... -short
# Run specific package
go test ./internal/api/... -v
go test ./internal/service/... -v
```
### Test Guidelines
1. **Table-driven tests**: Use for testing multiple scenarios
2. **Test naming**: `TestFunctionName_Scenario_ExpectedBehavior`
3. **Subtests**: Use `t.Run()` for related test cases
4. **Setup/Teardown**: Use `TestMain()` for package-level setup
5. **Parallel tests**: Use `t.Parallel()` when tests are independent
6. **Mock dependencies**: Never call real APIs in unit tests
7. **Test fixtures**: Store mock API responses in `test/fixtures/`
### Example Test Structure
```go
func TestServersList(t *testing.T) {
tests := []struct {
name string
response string
wantErr bool
wantCount int
}{
{
name: "successful list",
response: readFixture("servers_list.json"),
wantErr: false,
wantCount: 3,
},
{
name: "empty list",
response: "[]",
wantErr: false,
wantCount: 0,
},
{
name: "api error",
response: `{"error":"unauthorized"}`,
wantErr: true,
wantCount: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test implementation
})
}
}
```
### When Adding a New Command
**CHECKLIST** (must complete ALL items):
- [ ] Create command implementation in `cmd/`
- [ ] Create corresponding test file in `internal/service/*_test.go` or `internal/api/*_test.go`
- [ ] Test all flags and arguments
- [ ] Test all output formats (table, json, pretty)
- [ ] Test error cases (missing args, API errors, invalid input)
- [ ] Add integration test if command has complex workflow
- [ ] Update README.md with command documentation
- [ ] Run `go test ./internal/...` and ensure all tests pass
- [ ] Verify coverage: `go test ./internal/... -cover`
### CI/CD Integration
Tests run automatically on:
- Every pull request
- Every commit to main branch
- Before releases
**Pull requests will be blocked if:**
- Any test fails
- Coverage drops below 70%
- New code has no tests
## .cursorrules Context
The project follows Go 1.22+ idioms with standard library preference:
- Use `net/http` standard library (no external HTTP frameworks)
- Leverage Go 1.22 ServeMux features for any routing needs
- Follow RESTful patterns for API interactions
- Implement proper error handling with custom types when needed
- Use Go's concurrency features appropriately
- Write secure, efficient, and maintainable code
- **ALWAYS write tests** - see Testing Requirements section above
+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! 🚀
+123
View File
@@ -0,0 +1,123 @@
# How to Release Coolify CLI
This guide explains the release process for the Coolify CLI.
## Prerequisites
- Write access to the `coollabsio/coolify-cli` repository
- All changes merged to the target branch (`v4.x`)
- All tests passing (`go test ./internal/...`)
## Release Process
### 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:
- **Tag name**: `v1.x.x` (must start with `v`, e.g., `v1.2.3`)
- **Target**: `v4.x` (or your target branch)
3. **Release title**: `v1.x.x` (same as tag name)
4. **Description**: Write release notes describing:
- New features
- Bug fixes
- Breaking changes (if any)
- Example:
```markdown
## What's New
- Added support for database management
- Improved error messages for API failures
## Bug Fixes
- Fixed panic when config file is missing
## Breaking Changes
- None
```
5. Click "Publish release"
### 2. Automated Build Process
Once you publish the release:
1. GitHub Actions automatically triggers the `release-cli.yml` workflow
2. GoReleaser builds binaries for:
- **Linux**: amd64, arm64
- **macOS (Darwin)**: amd64, arm64
- **Windows**: amd64, arm64
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`
### 3. Verify the Release
After the workflow completes (usually 2-5 minutes):
1. Check the release page has all platform binaries
2. Test the install script:
```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
coolify version
```
3. Test the auto-update functionality:
```bash
# If you have an older version installed
coolify update
coolify version # Should show the new version
```
4. Verify the version matches your release
## Troubleshooting
### Build Failed
- Check the GitHub Actions logs at https://github.com/coollabsio/coolify-cli/actions
- Common issues:
- Syntax errors in Go code
- Test failures
- GoReleaser configuration issues
### Version Not Updating
- Ensure you committed the version change in `cmd/root.go`
- The tag must start with `v` (e.g., `v1.2.3`, not `1.2.3`)
- Check that the workflow has write permissions
### Install Script Not Finding New Version
- Wait a few minutes for GitHub's CDN to update
- Check that binaries were uploaded to the release
- Verify the tag format is correct (`v1.x.x`)
## Release Checklist
Before creating a release:
- [ ] All tests pass: `go test ./internal/...`
- [ ] Code is formatted: `go fmt ./...`
- [ ] Version updated in `cmd/root.go`
- [ ] Changes merged to `v4.x` branch
- [ ] Release notes prepared
After creating a release:
- [ ] GitHub Actions workflow completed successfully
- [ ] All platform binaries are present on the release page
- [ ] Install script downloads the new version
- [ ] `coolify version` returns the correct version
## Configuration Files
The release process uses these configuration files:
- `.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
- `internal/version/checker.go` - Contains `GetVersion()` function that returns the current version
- `coolify/main.go` - Binary entry point for `go install` support
## Notes
- The CLI has auto-update checking built-in (checks every 10 minutes)
- Users can manually update with `coolify update`
- Install script supports version pinning: `bash install.sh v1.2.3`
- Releases are immutable - if you need to fix something, create a new patch version
+493 -20
View File
@@ -2,46 +2,519 @@
## 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.
#### Windows (PowerShell)
## Configuration
```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`
### Cloud
2. Add the token with `coolify instances set token cloud <token>`
2. Add the token with `coolify context set-token cloud <token>`
### Self-hosted
2. Add the token with `coolify instances add -d <name> <fqdn> <token>`
> Replace `<name>` with the name you want to give to the instance.
2. Add the token with `coolify context add -d <context_name> <url> <token>`
> Replace `<context_name>` with the name you want to give to the context.
>
> Replace `<fqdn>` with the fully qualified domain name of your Coolify instance.
> Replace `<url>` with the fully qualified domain name of your Coolify instance.
Now you can use the CLI with the token you just added.
## Change default instance
You can change the default instance with `coolify instances set default <name>`
## Change default context
You can change the default context with `coolify context use <context_name>` or `coolify context set-default <context_name>`
## Currently Supported Commands
### Update
- `coolify update` - Update the CLI to the latest version
### Instances
- `coolify instances list` - List all instances
- `coolify instances add` - Create a new instance configuration
- `coolify instances remove` - Remove an instance configuration
- `coolify instances get` - Get an instance configuration
- `coolify instances set <default>|<token>` - Set an instance as default or set a token for an instance
- `coolify instances version` - Get the version of the Coolify API for an instance
### Configuration
- `coolify config` - Show configuration file location
### 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
- `-d, --default` - Set as default context
- `-f, --force` - Force overwrite if context already exists
- `coolify context delete <context_name>` - Delete a context
- `coolify context get <context_name>` - Get details of a specific context
- `coolify context set-token <context_name> <token>` - Update the API token for a context
- `coolify context set-default <context_name>` - Set a context as the default
- `coolify context update <context_name>` - Update a context's properties
- `--name <new_name>` - Change the context name
- `--url <new_url>` - Change the context URL
- `--token <new_token>` - Change the context token
- `coolify context use <context_name>` - Switch to a different context (set as default)
- `coolify context 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` - Get a server
- `--resources` - Get the resources and their status of a server
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
- `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)
- `--validate` - Validate server immediately after adding
- `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
- `coolify projects get <uuid>` - Get project environments
### Resources
- `coolify resources list` - List all resources
### Applications
- `coolify app list` - List all applications
- `coolify app get <uuid>` - Get application details
- `coolify app update <uuid>` - Update application configuration
- `--name <name>` - Application name
- `--description <description>` - Application description
- `--git-branch <branch>` - Git branch
- `--git-repository <url>` - Git repository URL
- `--domains <domains>` - Domains (comma-separated)
- `--build-command <cmd>` - Build command
- `--start-command <cmd>` - Start command
- `--install-command <cmd>` - Install command
- `--base-directory <path>` - Base directory
- `--publish-directory <path>` - Publish directory
- `--dockerfile <content>` - Dockerfile content
- `--docker-image <image>` - Docker image name
- `--docker-tag <tag>` - Docker image tag
- `--ports-exposes <ports>` - Exposed ports
- `--ports-mappings <mappings>` - Port mappings
- `--health-check-enabled` - Enable health check
- `--health-check-path <path>` - Health check path
- `coolify app delete <uuid>` - Delete an application
- `-f, --force` - Skip confirmation prompt
- `coolify app start <uuid>` - Start an application
- `coolify app stop <uuid>` - Stop an application
- `coolify app restart <uuid>` - Restart an application
- `coolify app logs <uuid>` - Get application logs
#### Application Environment Variables
- `coolify app env list <app_uuid>` - List all environment variables
- `coolify app env get <app_uuid> <env_uuid_or_key>` - Get a specific environment variable
- `coolify app env create <app_uuid>` - Create a new environment variable
- `--key <key>` - Variable key (required)
- `--value <value>` - Variable value (required)
- `--preview` - Available in preview deployments
- `--build-time` - Available at build time
- `--is-literal` - Treat value as literal (don't interpolate variables)
- `--is-multiline` - Value is multiline
- `coolify app env update <app_uuid> <env_uuid>` - Update an environment variable
- `coolify app env delete <app_uuid> <env_uuid>` - Delete an environment variable
- `coolify app env sync <app_uuid>` - Sync environment variables from a .env file
- `--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
- `coolify database get <uuid>` - Get database details
- `coolify database create <type>` - Create a new database
- Supported types: `postgresql`, `mysql`, `mariadb`, `mongodb`, `redis`, `keydb`, `clickhouse`, `dragonfly`
- `--server-uuid <uuid>` - Server UUID (required)
- `--project-uuid <uuid>` - Project UUID (required)
- `--environment-name <name>` - Environment name (required unless using --environment-uuid)
- `--environment-uuid <uuid>` - Environment UUID (required unless using --environment-name)
- `--destination-uuid <uuid>` - Destination UUID if server has multiple destinations
- `--name <name>` - Database name
- `--description <description>` - Database description
- `--image <image>` - Docker image
- `--instant-deploy` - Deploy immediately after creation
- `--is-public` - Make database publicly accessible
- `--public-port <port>` - Public port number
- `--limits-memory <size>` - Memory limit (e.g., '512m', '2g')
- `--limits-cpus <cpus>` - CPU limit (e.g., '0.5', '2')
- Database-specific flags (postgres-user, mysql-root-password, etc.)
- `coolify database update <uuid>` - Update database configuration
- `coolify database delete <uuid>` - Delete a database
- `--delete-configurations` - Delete configurations (default: true)
- `--delete-volumes` - Delete volumes (default: true)
- `--docker-cleanup` - Run docker cleanup (default: true)
- `--delete-connected-networks` - Delete connected networks (default: true)
- `coolify database start <uuid>` - Start a database
- `coolify database stop <uuid>` - Stop a database
- `coolify database restart <uuid>` - Restart a database
#### Database Backups
- `coolify database backup list <database_uuid>` - List all backup configurations
- `coolify database backup create <database_uuid>` - Create a new backup configuration
- `--frequency <cron>` - Backup frequency (cron expression)
- `--enabled` - Enable backup schedule
- `--save-s3` - Save backups to S3
- `--s3-storage-uuid <uuid>` - S3 storage UUID
- `--databases-to-backup <list>` - Comma-separated list of databases to backup
- `--dump-all` - Dump all databases
- `--retention-amount-local <n>` - Number of backups to retain locally
- `--retention-days-local <n>` - Days to retain backups locally
- `--retention-storage-local <size>` - Max storage for local backups (e.g., '1GB', '500MB')
- `--retention-amount-s3 <n>` - Number of backups to retain in S3
- `--retention-days-s3 <n>` - Days to retain backups in S3
- `--retention-storage-s3 <size>` - Max storage for S3 backups (e.g., '1GB', '500MB')
- `--timeout <seconds>` - Backup timeout in seconds
- `--disable-local` - Disable local backup storage
- `coolify database backup update <database_uuid> <backup_uuid>` - Update a backup configuration
- `coolify database backup delete <database_uuid> <backup_uuid>` - Delete a backup configuration
- `coolify database backup trigger <database_uuid> <backup_uuid>` - Trigger an immediate backup
- `coolify database backup executions <database_uuid> <backup_uuid>` - List backup executions
- `coolify database backup delete-execution <database_uuid> <backup_uuid> <execution_uuid>` - Delete a backup execution
### Services
- `coolify service list` - List all services
- `coolify service get <uuid>` - Get service details
- `coolify service start <uuid>` - Start a service
- `coolify service stop <uuid>` - Stop a service
- `coolify service restart <uuid>` - Restart a service
- `coolify service delete <uuid>` - Delete a service
#### Service Environment Variables
- `coolify service env list <service_uuid>` - List all environment variables
- `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 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
- `-f, --force` - Force deployment
- `coolify deploy name <name>` - Deploy a resource by name
- `-f, --force` - Force deployment
- `coolify deploy batch <name1,name2,...>` - Deploy multiple resources at once
- `-f, --force` - Force all deployments
- `coolify deploy list` - List all deployments
- `coolify deploy get <uuid>` - Get deployment details
- `coolify deploy cancel <uuid>` - Cancel a deployment
- `-f, --force` - Skip confirmation prompt
### GitHub Apps
- `coolify github list` - List all GitHub App integrations
- `coolify github get <app_uuid>` - Get GitHub App details
- `coolify github create` - Create a new GitHub App integration
- `--name <name>` - GitHub App name (required)
- `--api-url <url>` - GitHub API URL (required, e.g., https://api.github.com)
- `--html-url <url>` - GitHub HTML URL (required, e.g., https://github.com)
- `--app-id <id>` - GitHub App ID (required)
- `--installation-id <id>` - GitHub Installation ID (required)
- `--client-id <id>` - GitHub OAuth Client ID (required)
- `--client-secret <secret>` - GitHub OAuth Client Secret (required)
- `--private-key-uuid <uuid>` - UUID of existing private key (required)
- `--organization <org>` - GitHub organization
- `--custom-user <user>` - Custom user for SSH (default: git)
- `--custom-port <port>` - Custom port for SSH (default: 22)
- `--webhook-secret <secret>` - GitHub Webhook Secret
- `--system-wide` - Is this app system-wide (cloud only)
- `coolify github update <app_uuid>` - Update a GitHub App
- `coolify github delete <app_uuid>` - Delete a GitHub App
- `-f, --force` - Skip confirmation prompt
- `coolify github repos <app_uuid>` - List repositories accessible by a GitHub App
- `coolify github branches <app_uuid> <owner/repo>` - List branches for a repository
### Teams
- `coolify team list` - List all teams
- `coolify team get <team_id>` - Get team details
- `coolify team current` - Get current team
- `coolify team members list [team_id]` - List team members
### Private Keys
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:
- `--context <name>` - Use a specific context instead of default
- `--host <fqdn>` - Override the Coolify instance hostname
- `--token <token>` - Override the authentication token
- `--format <format>` - Output format: `table` (default), `json`, or `pretty`
- `-s, --show-sensitive` - Show sensitive information (tokens, IPs, etc.)
- `-f, --force` - Force operation (skip confirmations)
- `--debug` - Enable debug mode
## Examples
### Multi-Environment Workflows
```bash
# Add multiple contexts
coolify context add prod https://prod.coolify.io <prod-token>
coolify context add staging https://staging.coolify.io <staging-token>
coolify context add dev https://dev.coolify.io <dev-token>
# Set default
coolify context use prod
# Use different contexts
coolify --context=staging servers list
coolify --context=prod deploy name api
coolify --context=dev resources list
# Default context (prod in this case)
coolify servers list
```
### Application Management
```bash
# List all applications
coolify app list
# Get application details
coolify app get <uuid>
# Manage application lifecycle
coolify app start <uuid>
coolify app stop <uuid>
coolify app restart <uuid>
# View application logs
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
```bash
# List databases
coolify database list
# Create a PostgreSQL database
coolify database create postgresql \
--server-uuid <server-uuid> \
--project-uuid <project-uuid> \
--name mydb \
--instant-deploy
# Manage database lifecycle
coolify database start <uuid>
coolify database stop <uuid>
coolify database restart <uuid>
# Backup management
coolify database backup list <database-uuid>
coolify database backup create <database-uuid> \
--frequency "0 2 * * *" \
--enabled \
--save-s3 \
--retention-days-locally 7
coolify database backup trigger <database-uuid> <backup-uuid>
```
### Service Management
```bash
# List services
coolify service list
# Get service details
coolify service get <uuid>
# Manage services
coolify service start <uuid>
coolify service restart <uuid>
# Environment variables (same as applications)
coolify service env sync <uuid> --file .env
```
### Deploy Workflows
```bash
# Deploy single app by name (easier than UUID)
coolify deploy name my-application
# Deploy multiple apps at once
coolify deploy batch api,worker,frontend
# Force deploy with specific context
coolify --context=prod deploy batch api,worker --force
# Traditional UUID deployment still works
coolify deploy uuid abc123-def456-...
# Monitor deployments
coolify deploy list
coolify deploy get <deployment-uuid>
# Cancel a deployment
coolify deploy cancel <deployment-uuid>
```
### GitHub Apps Integration
```bash
# List GitHub Apps
coolify github list
# Create a GitHub App integration
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 "secret" \
--private-key-uuid <key-uuid>
# List repositories accessible by the app
coolify github repos <app-uuid>
# List branches for a repository
coolify github branches <app-uuid> owner/repo
# Delete a GitHub App
coolify github delete <app-uuid>
```
### Team Management
```bash
# List teams
coolify team list
# Get current team
coolify team current
# List team members
coolify team members list
```
### Server Management
```bash
# List servers in production
coolify --context=prod server list
# Add a server with validation
coolify server add myserver 192.168.1.100 <key-uuid> --validate
# Get server details with resources
coolify server get <uuid> --resources
```
## Output Formats
The CLI supports three output formats:
```bash
# Table format (default, human-readable)
coolify server list
# JSON format (for scripts)
coolify server list --format=json
# Pretty JSON (for debugging)
coolify server list --format=pretty
```
## Architecture
This CLI follows a clean architecture with:
- **Service Layer**: Business logic and API interactions
- **Output Layer**: Consistent formatting across all commands
- **Config Layer**: Multi-context configuration management
- **Models Layer**: Type-safe data structures
## Development
```bash
# Build
go build -o coolify ./coolify
# Run tests
go test ./...
# Run with coverage
go test -cover ./...
# Install locally
go install ./coolify
```
## Contributing
Contributions are welcome!
## License
MIT
+45
View File
@@ -0,0 +1,45 @@
package application
import (
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/cmd/application/env"
)
// NewAppCommand creates the app parent command
func NewAppCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "app",
Aliases: []string{"apps", "application", "applications"},
Short: "Application related commands",
Long: `Manage Coolify applications - list, get, create, update, delete, and control application lifecycle.`,
}
// Add main subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewUpdateCommand())
cmd.AddCommand(NewDeleteCommand())
cmd.AddCommand(NewStartCommand())
cmd.AddCommand(NewStopCommand())
cmd.AddCommand(NewRestartCommand())
cmd.AddCommand(NewLogsCommand())
cmd.AddCommand(NewDeploymentsCommand())
// Add env subcommand with its children
envCmd := &cobra.Command{
Use: "env",
Aliases: []string{"envs", "environment"},
Short: "Manage application environment variables",
Long: `List and manage environment variables for applications. All commands require the application UUID first to establish context.`,
}
envCmd.AddCommand(env.NewListEnvCommand())
envCmd.AddCommand(env.NewGetEnvCommand())
envCmd.AddCommand(env.NewCreateEnvCommand())
envCmd.AddCommand(env.NewUpdateEnvCommand())
envCmd.AddCommand(env.NewDeleteEnvCommand())
envCmd.AddCommand(env.NewSyncEnvCommand())
cmd.AddCommand(envCmd)
return cmd
}
+57
View File
@@ -0,0 +1,57 @@
package application
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 <uuid>",
Short: "Delete an application",
Long: `Delete an application. This action cannot be undone.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
force, _ := cmd.Flags().GetBool("force")
if !force {
var response string
fmt.Printf("Are you sure you want to delete application %s? This cannot be undone. (yes/no): ", uuid)
_, 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.")
return nil
}
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
err = appSvc.Delete(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to delete application: %w", err)
}
fmt.Printf("Application %s deleted successfully.\n", uuid)
return nil
},
}
cmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
return cmd
}
+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"
}
+84
View File
@@ -0,0 +1,84 @@
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 NewCreateEnvCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create <app_uuid>",
Short: "Create an environment variable for an application",
Long: `Create a new environment variable for a specific application. Use --key and --value flags to specify the variable.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
key, _ := cmd.Flags().GetString("key")
value, _ := cmd.Flags().GetString("value")
isBuildTime, _ := cmd.Flags().GetBool("build-time")
isPreview, _ := cmd.Flags().GetBool("preview")
isLiteral, _ := cmd.Flags().GetBool("is-literal")
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
isRuntime, _ := cmd.Flags().GetBool("runtime")
if key == "" {
return fmt.Errorf("--key is required")
}
if value == "" {
return fmt.Errorf("--value is required")
}
req := &models.EnvironmentVariableCreateRequest{
Key: key,
Value: value,
}
if cmd.Flags().Changed("build-time") {
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("is-multiline") {
req.IsMultiline = &isMultiline
}
if cmd.Flags().Changed("runtime") {
req.IsRuntime = &isRuntime
}
appSvc := service.NewApplicationService(client)
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("UUID: %s\n", env.UUID)
return nil
},
}
cmd.Flags().String("key", "", "Environment variable key (required)")
cmd.Flags().String("value", "", "Environment variable value (required)")
cmd.Flags().Bool("build-time", true, "Available at build time (default: true)")
cmd.Flags().Bool("preview", false, "Available in preview deployments")
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
return cmd
}
+59
View File
@@ -0,0 +1,59 @@
package env
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
func NewDeleteEnvCommand() *cobra.Command {
deleteEnvCmd := &cobra.Command{
Use: "delete <app_uuid> <env_uuid>",
Short: "Delete an environment variable",
Long: `Delete an environment variable from an application. First UUID is the application, second is the specific environment variable to delete.`,
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
appUUID := args[0]
envUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
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): ")
_, 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.")
return nil
}
}
appSvc := service.NewApplicationService(client)
err = appSvc.DeleteEnv(ctx, appUUID, envUUID)
if err != nil {
return fmt.Errorf("failed to delete environment variable: %w", err)
}
fmt.Println("Environment variable deleted successfully.")
return nil
},
}
deleteEnvCmd.Flags().Bool("force", false, "Skip confirmation prompt")
return deleteEnvCmd
}
+60
View File
@@ -0,0 +1,60 @@
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 NewGetEnvCommand() *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, "<app_uuid> <env_uuid_or_key>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
appUUID := args[0]
envUUIDOrKey := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
// First try to get by the identifier directly
env, err := appSvc.GetEnv(ctx, appUUID, envUUIDOrKey)
if err != nil {
return fmt.Errorf("failed to get environment variable: %w", err)
}
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
if !showSensitive {
env.Value = "********"
if env.RealValue != nil {
masked := "********"
env.RealValue = &masked
}
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(env)
},
}
return cmd
}
+87
View File
@@ -0,0 +1,87 @@
package env
import (
"fmt"
"sort"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/models"
"github.com/coollabsio/coolify-cli/internal/output"
"github.com/coollabsio/coolify-cli/internal/service"
)
func NewListEnvCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "list <app_uuid>",
Short: "List all environment variables for an application",
Long: `List all environment variables for a specific application. By default, only non-preview environment variables are shown. Use --preview to show preview environment variables instead, or --all to show all variables (non-preview first, then preview).`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
envs, err := appSvc.ListEnvs(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to list environment variables: %w", err)
}
// 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")
if !showSensitive {
for i := range envs {
envs[i].Value = "********"
if envs[i].RealValue != nil {
masked := "********"
envs[i].RealValue = &masked
}
}
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(envs)
},
}
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
}
+159
View File
@@ -0,0 +1,159 @@
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 NewSyncEnvCommand() *cobra.Command {
syncEnvCmd := &cobra.Command{
Use: "sync <app_uuid>",
Short: "Sync environment variables from a .env file",
Long: `Sync environment variables from a .env file. This command intelligently:
- Updates existing environment variables with new values
- Creates new environment variables that don't exist yet
- Uses efficient bulk operations where possible
Example: coolify app env sync abc123 --file .env.production`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := 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")
}
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)
if err != nil {
return fmt.Errorf("failed to parse .env file: %w", err)
}
if len(envVars) == 0 {
fmt.Println("No environment variables found in file.")
return nil
}
fmt.Printf("Found %d environment variables in file. Syncing...\n", len(envVars))
// Fetch existing environment variables
appSvc := service.NewApplicationService(client)
existingEnvs, err := appSvc.ListEnvs(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to list existing environment variables: %w", err)
}
// Build a map of existing env vars by key
existingMap := make(map[string]models.EnvironmentVariable)
for _, env := range existingEnvs {
existingMap[env.Key] = env
}
// Separate into updates and creates
var toUpdate []models.EnvironmentVariableCreateRequest
var toCreate []models.EnvironmentVariableCreateRequest
for _, envVar := range envVars {
req := models.EnvironmentVariableCreateRequest{
Key: envVar.Key,
Value: envVar.Value,
}
// Apply flags if explicitly provided
if cmd.Flags().Changed("build-time") {
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("runtime") {
req.IsRuntime = &isRuntime
}
// Auto-detect multiline values
if strings.Contains(envVar.Value, "\n") {
multiline := true
req.IsMultiline = &multiline
}
if _, exists := existingMap[envVar.Key]; exists {
toUpdate = append(toUpdate, req)
} else {
toCreate = append(toCreate, req)
}
}
updateCount := 0
createCount := 0
failCount := 0
// Perform bulk update if there are vars to update
if len(toUpdate) > 0 {
fmt.Printf("Updating %d existing variables...\n", len(toUpdate))
bulkReq := &service.BulkUpdateEnvsRequest{
Data: toUpdate,
}
_, err := appSvc.BulkUpdateEnvs(ctx, uuid, bulkReq)
if err != nil {
fmt.Printf(" ✗ Bulk update failed: %v\n", err)
failCount += len(toUpdate)
} else {
updateCount = len(toUpdate)
fmt.Printf(" ✓ Successfully updated %d variables\n", updateCount)
}
}
// Create new variables one by one
if len(toCreate) > 0 {
fmt.Printf("Creating %d new variables...\n", len(toCreate))
for _, req := range toCreate {
_, err := appSvc.CreateEnv(ctx, uuid, &req)
if err != nil {
fmt.Printf(" ✗ Failed to create '%s': %v\n", req.Key, err)
failCount++
} else {
fmt.Printf(" ✓ Created '%s'\n", req.Key)
createCount++
}
}
}
fmt.Printf("\nSync complete: %d updated, %d created, %d failed\n", updateCount, createCount, failCount)
if failCount > 0 {
return fmt.Errorf("some environment variables failed to sync")
}
return nil
},
}
syncEnvCmd.Flags().StringP("file", "f", "", "Path to .env file (required)")
syncEnvCmd.Flags().Bool("build-time", 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
}
+85
View File
@@ -0,0 +1,85 @@
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 NewUpdateEnvCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <app_uuid> <env_uuid>",
Short: "Update an environment variable",
Long: `Update an existing environment variable. First UUID is the application, second is the specific environment variable to update.`,
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
appUUID := args[0]
envUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
req := &models.EnvironmentVariableUpdateRequest{
UUID: envUUID,
}
if cmd.Flags().Changed("key") {
key, _ := cmd.Flags().GetString("key")
req.Key = &key
}
if cmd.Flags().Changed("value") {
value, _ := cmd.Flags().GetString("value")
req.Value = &value
}
if cmd.Flags().Changed("build-time") {
isBuildTime, _ := cmd.Flags().GetBool("build-time")
req.IsBuildTime = &isBuildTime
}
if cmd.Flags().Changed("preview") {
isPreview, _ := cmd.Flags().GetBool("preview")
req.IsPreview = &isPreview
}
if cmd.Flags().Changed("is-literal") {
isLiteral, _ := cmd.Flags().GetBool("is-literal")
req.IsLiteral = &isLiteral
}
if cmd.Flags().Changed("is-multiline") {
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
req.IsMultiline = &isMultiline
}
if cmd.Flags().Changed("runtime") {
isRuntime, _ := cmd.Flags().GetBool("runtime")
req.IsRuntime = &isRuntime
}
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsPreview == nil && req.IsLiteral == nil && req.IsMultiline == nil && req.IsRuntime == nil {
return fmt.Errorf("at least one field must be provided to update")
}
appSvc := service.NewApplicationService(client)
env, err := appSvc.UpdateEnv(ctx, appUUID, req)
if err != nil {
return fmt.Errorf("failed to update environment variable: %w", err)
}
fmt.Printf("Environment variable '%s' updated successfully.\n", env.Key)
return nil
},
}
cmd.Flags().String("key", "", "New environment variable key")
cmd.Flags().String("value", "", "New environment variable value")
cmd.Flags().Bool("build-time", true, "Available at build time (default: true)")
cmd.Flags().Bool("preview", false, "Available in preview deployments")
cmd.Flags().Bool("is-literal", false, "Treat value as literal")
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
return cmd
}
+47
View File
@@ -0,0 +1,47 @@
package application
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 <uuid>",
Short: "Get application details by UUID",
Long: `Retrieve detailed information about a specific application.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
app, err := appSvc.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get application: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(app)
},
}
}
+70
View File
@@ -0,0 +1,70 @@
package application
import (
"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"
)
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all applications",
Long: `List all applications in Coolify.`,
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)
}
appSvc := service.NewApplicationService(client)
apps, err := appSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list applications: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// For JSON/pretty formats, return the full application structure
if format != output.FormatTable {
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(apps)
}
// For table format, convert to simplified rows
var rows []models.ApplicationListItem
for _, app := range apps {
rows = append(rows, models.ApplicationListItem{
UUID: app.UUID,
Name: app.Name,
Description: app.Description,
Status: app.Status,
GitBranch: app.GitBranch,
FQDN: app.FQDN,
})
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(rows)
},
}
}
+86
View File
@@ -0,0 +1,86 @@
package application
import (
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
func NewLogsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "logs <uuid>",
Short: "Get application logs",
Long: `Retrieve logs for an application. Use --follow to continuously stream new logs.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
lines, _ := cmd.Flags().GetInt("lines")
follow, _ := cmd.Flags().GetBool("follow")
appSvc := service.NewApplicationService(client)
if !follow {
resp, err := appSvc.Logs(ctx, uuid, lines)
if err != nil {
return fmt.Errorf("failed to get logs: %w", err)
}
fmt.Print(resp.Logs)
return nil
}
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
lastLogs := ""
resp, err := appSvc.Logs(ctx, uuid, lines)
if err != nil {
return fmt.Errorf("failed to get logs: %w", err)
}
fmt.Print(resp.Logs)
lastLogs = resp.Logs
for {
select {
case <-sigChan:
fmt.Println("\nStopping log follow...")
return nil
case <-ticker.C:
resp, err := appSvc.Logs(ctx, uuid, lines)
if err != nil {
continue
}
if resp.Logs != lastLogs {
if len(resp.Logs) > len(lastLogs) && strings.HasPrefix(resp.Logs, lastLogs) {
fmt.Print(resp.Logs[len(lastLogs):])
} else {
fmt.Print(resp.Logs)
}
lastLogs = resp.Logs
}
}
}
},
}
cmd.Flags().IntP("lines", "n", 100, "Number of log lines to retrieve")
cmd.Flags().BoolP("follow", "f", false, "Follow log output (like tail -f)")
return cmd
}
+37
View File
@@ -0,0 +1,37 @@
package application
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
func NewRestartCommand() *cobra.Command {
return &cobra.Command{
Use: "restart <uuid>",
Short: "Restart an application",
Long: `Restart a running application.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
resp, err := appSvc.Restart(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to restart application: %w", err)
}
fmt.Println(resp.Message)
return nil
},
}
}
+48
View File
@@ -0,0 +1,48 @@
package application
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
func NewStartCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "start <uuid>",
Aliases: []string{"deploy"},
Short: "Start an application",
Long: `Start an application (initiates a deployment).`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
instantDeploy, _ := cmd.Flags().GetBool("instant-deploy")
appSvc := service.NewApplicationService(client)
resp, err := appSvc.Start(ctx, uuid, force, instantDeploy)
if err != nil {
return fmt.Errorf("failed to start application: %w", err)
}
fmt.Println(resp.Message)
if resp.DeploymentUUID != nil && *resp.DeploymentUUID != "" {
fmt.Printf("Deployment UUID: %s\n", *resp.DeploymentUUID)
}
return nil
},
}
cmd.Flags().Bool("force", false, "Force rebuild")
cmd.Flags().Bool("instant-deploy", false, "Instant deploy (skip queuing)")
return cmd
}
+37
View File
@@ -0,0 +1,37 @@
package application
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
func NewStopCommand() *cobra.Command {
return &cobra.Command{
Use: "stop <uuid>",
Short: "Stop an application",
Long: `Stop a running application.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
appSvc := service.NewApplicationService(client)
resp, err := appSvc.Stop(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to stop application: %w", err)
}
fmt.Println(resp.Message)
return nil
},
}
}
+161
View File
@@ -0,0 +1,161 @@
package application
import (
"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"
)
func NewUpdateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <uuid>",
Short: "Update application configuration",
Long: `Update configuration for a specific application. Only specified fields will be updated.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
req := models.ApplicationUpdateRequest{}
hasUpdates := false
if cmd.Flags().Changed("name") {
name, _ := cmd.Flags().GetString("name")
req.Name = &name
hasUpdates = true
}
if cmd.Flags().Changed("description") {
desc, _ := cmd.Flags().GetString("description")
req.Description = &desc
hasUpdates = true
}
if cmd.Flags().Changed("git-branch") {
branch, _ := cmd.Flags().GetString("git-branch")
req.GitBranch = &branch
hasUpdates = true
}
if cmd.Flags().Changed("git-repository") {
repo, _ := cmd.Flags().GetString("git-repository")
req.GitRepository = &repo
hasUpdates = true
}
if cmd.Flags().Changed("domains") {
domains, _ := cmd.Flags().GetString("domains")
req.Domains = &domains
hasUpdates = true
}
if cmd.Flags().Changed("build-command") {
buildCmd, _ := cmd.Flags().GetString("build-command")
req.BuildCommand = &buildCmd
hasUpdates = true
}
if cmd.Flags().Changed("start-command") {
startCmd, _ := cmd.Flags().GetString("start-command")
req.StartCommand = &startCmd
hasUpdates = true
}
if cmd.Flags().Changed("install-command") {
installCmd, _ := cmd.Flags().GetString("install-command")
req.InstallCommand = &installCmd
hasUpdates = true
}
if cmd.Flags().Changed("base-directory") {
baseDir, _ := cmd.Flags().GetString("base-directory")
req.BaseDirectory = &baseDir
hasUpdates = true
}
if cmd.Flags().Changed("publish-directory") {
publishDir, _ := cmd.Flags().GetString("publish-directory")
req.PublishDirectory = &publishDir
hasUpdates = true
}
if cmd.Flags().Changed("dockerfile") {
dockerfile, _ := cmd.Flags().GetString("dockerfile")
req.Dockerfile = &dockerfile
hasUpdates = true
}
if cmd.Flags().Changed("docker-image") {
image, _ := cmd.Flags().GetString("docker-image")
req.DockerRegistryImageName = &image
hasUpdates = true
}
if cmd.Flags().Changed("docker-tag") {
tag, _ := cmd.Flags().GetString("docker-tag")
req.DockerRegistryImageTag = &tag
hasUpdates = true
}
if cmd.Flags().Changed("ports-exposes") {
ports, _ := cmd.Flags().GetString("ports-exposes")
req.PortsExposes = &ports
hasUpdates = true
}
if cmd.Flags().Changed("ports-mappings") {
ports, _ := cmd.Flags().GetString("ports-mappings")
req.PortsMappings = &ports
hasUpdates = true
}
if cmd.Flags().Changed("health-check-enabled") {
enabled, _ := cmd.Flags().GetBool("health-check-enabled")
req.HealthCheckEnabled = &enabled
hasUpdates = true
}
if cmd.Flags().Changed("health-check-path") {
path, _ := cmd.Flags().GetString("health-check-path")
req.HealthCheckPath = &path
hasUpdates = true
}
if !hasUpdates {
return fmt.Errorf("no fields to update. Use --help to see available flags")
}
appSvc := service.NewApplicationService(client)
app, err := appSvc.Update(ctx, uuid, req)
if err != nil {
return fmt.Errorf("failed to update application: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(app)
},
}
cmd.Flags().String("name", "", "Application name")
cmd.Flags().String("description", "", "Application description")
cmd.Flags().String("git-branch", "", "Git branch")
cmd.Flags().String("git-repository", "", "Git repository URL")
cmd.Flags().String("domains", "", "Domains (comma-separated)")
cmd.Flags().String("build-command", "", "Build command")
cmd.Flags().String("start-command", "", "Start command")
cmd.Flags().String("install-command", "", "Install command")
cmd.Flags().String("base-directory", "", "Base directory")
cmd.Flags().String("publish-directory", "", "Publish directory")
cmd.Flags().String("dockerfile", "", "Dockerfile content")
cmd.Flags().String("docker-image", "", "Docker image name")
cmd.Flags().String("docker-tag", "", "Docker image tag")
cmd.Flags().String("ports-exposes", "", "Exposed ports")
cmd.Flags().String("ports-mappings", "", "Port mappings")
cmd.Flags().Bool("health-check-enabled", false, "Enable health check")
cmd.Flags().String("health-check-path", "", "Health check path")
return cmd
}
+97
View File
@@ -0,0 +1,97 @@
package completion
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
)
func NewCompletionsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "completion <shell>",
Short: "Output shell completion code for the specified shell",
Long: `To load completions:
### Bash
To load completions into the current shell execute:
source <(coolify completion bash)
In order to make the completions permanent, append the line above to
your .bashrc.
### Zsh
If shell completions are not already enabled for your environment need
to enable them. Add the following line to your ~/.zshrc file:
autoload -Uz compinit; compinit
To load completions for each session execute the following commands:
mkdir -p ~/.config/coolify/completion/zsh
coolify completion zsh > ~/.config/coolify/completion/zsh/_coolify
Finally add the following line to your ~/.zshrc file, *before* you
call the compinit function:
fpath+=(~/.config/coolify/completion/zsh)
In the end your ~/.zshrc file should contain the following two lines
in the order given here.
fpath+=(~/.config/coolify/completion/zsh)
# ... anything else that needs to be done before compinit
autoload -Uz compinit; compinit
# ...
You will need to start a new shell for this setup to take effect.
### Fish
To load completions into the current shell execute:
coolify completion fish | source
In order to make the completions permanent execute once:
coolify completion fish > ~/.config/fish/completions/coolify.fish
### PowerShell:
To load completions into the current shell execute:
PS> coolify completion powershell | Out-String | Invoke-Expression
To load completions for every new session, run
and source this file from your PowerShell profile.
PS> coolify completion powershell > coolify.ps1
`,
Args: cli.ExactArgs(1, "<shell>"),
ValidArgs: []string{"bash", "fish", "zsh", "powershell"},
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) error {
var err error
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletion(os.Stdout)
case "fish":
err = cmd.Root().GenFishCompletion(os.Stdout, true)
case "zsh":
err = cmd.Root().GenZshCompletion(os.Stdout)
case "powershell":
err = cmd.Root().GenPowerShellCompletion(os.Stdout)
default:
err = fmt.Errorf("Unsupported shell: %s", args[0])
}
return err
},
}
return cmd
}
+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)
}
}
+93
View File
@@ -0,0 +1,93 @@
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"
)
// NewAddCommand creates the add command
func NewAddCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "add <context_name> <url> <token>",
Example: `context add myserver https://coolify.example.com your-api-token`,
Args: cli.ExactArgs(3, "<context_name> <url> <token>"),
Short: "Add a new context",
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
host := args[1]
token := args[2]
force, _ := cmd.Flags().GetBool("force")
setDefault, _ := cmd.Flags().GetBool("default")
instances := viper.Get("instances").([]any)
// Check if instance already exists
for _, instance := range instances {
instanceMap := instance.(map[string]any)
if instanceMap["name"] == name {
if force {
instanceMap["token"] = token
if setDefault {
// Remove default from all instances
for _, inst := range instances {
instMap := inst.(map[string]any)
instMap["default"] = false
}
instanceMap["default"] = true
fmt.Printf("%s already exists. Force overwriting. Setting it as default.\n", name)
} else {
fmt.Printf("%s already exists. Force overwriting.\n", name)
}
viper.Set("instances", instances)
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 nil
}
}
// Add new instance
newInstance := config.Instance{
Name: name,
FQDN: host,
Token: token,
Default: false,
}
if setDefault {
// Remove default from all instances
for _, inst := range instances {
instMap := inst.(map[string]any)
instMap["default"] = false
}
newInstance.Default = true
fmt.Printf("Context '%s' added and set as default.\n", newInstance.Name)
} else {
fmt.Printf("Context '%s' added successfully.\n", newInstance.Name)
}
instances = append(instances, newInstance)
viper.Set("instances", instances)
if err := viper.WriteConfig(); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
return nil
},
}
cmd.Flags().BoolP("default", "d", false, "Set as default context")
cmd.Flags().BoolP("force", "f", false, "Force overwrite if context already exists")
return cmd
}
+28
View File
@@ -0,0 +1,28 @@
package context
import (
"github.com/spf13/cobra"
)
// NewContextCommand creates the context parent command
func NewContextCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "context",
Short: "Manage Coolify contexts",
Long: `Manage Coolify contexts. A context contains the configuration (URL and token) for connecting to Coolify.`,
}
// Add subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewAddCommand())
cmd.AddCommand(NewDeleteCommand())
cmd.AddCommand(NewUseCommand())
cmd.AddCommand(NewUpdateCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewSetTokenCommand())
cmd.AddCommand(NewSetDefaultCommand())
cmd.AddCommand(NewVersionCommand())
cmd.AddCommand(NewVerifyCommand())
return cmd
}
+54
View File
@@ -0,0 +1,54 @@
package context
import (
"fmt"
"slices"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewDeleteCommand creates the delete command
func NewDeleteCommand() *cobra.Command {
return &cobra.Command{
Use: "delete <context_name>",
Example: `context delete myserver`,
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Delete a context",
RunE: func(_ *cobra.Command, args []string) error {
Name := args[0]
instances := viper.Get("instances").([]interface{})
for i, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
instances = slices.Delete(instances, i, i+1)
viper.Set("instances", instances)
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)
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 {
fmt.Printf("Context '%s' deleted. No contexts remaining.\n", Name)
}
} else {
fmt.Printf("Context '%s' deleted.\n", Name)
}
return nil
}
}
return fmt.Errorf("context '%s' not found", Name)
},
}
}
+70
View File
@@ -0,0 +1,70 @@
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"
)
// NewGetCommand creates the get command
func NewGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <context_name>",
Example: `context get myserver`,
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Get details of a specific context",
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
instancesRaw := viper.Get("instances")
if instancesRaw == nil {
instancesRaw = []any{}
}
instancesInterface := instancesRaw.([]any)
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// Convert interface{} to config.Instance structs
var instances []config.Instance
for _, item := range instancesInterface {
itemMap := item.(map[string]any)
instance := config.Instance{
Name: getString(itemMap, "name"),
FQDN: getString(itemMap, "fqdn"),
Token: getString(itemMap, "token"),
Default: getBool(itemMap, "default"),
}
instances = append(instances, instance)
}
// If a name was provided, filter to that single instance
var results []config.Instance
for _, inst := range instances {
if inst.Name == name {
results = append(results, inst)
break
}
}
if len(results) == 0 {
return fmt.Errorf("Context '%s' not found", name)
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(results)
},
}
}
+70
View File
@@ -0,0 +1,70 @@
package context
import (
"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
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all configured contexts",
RunE: func(cmd *cobra.Command, _ []string) error {
// Get instances from viper (returns []interface{})
instancesRaw := viper.Get("instances")
if instancesRaw == nil {
instancesRaw = []interface{}{}
}
instancesInterface := instancesRaw.([]interface{})
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// Convert interface{} to config.Instance structs
var instances []config.Instance
for _, item := range instancesInterface {
itemMap := item.(map[string]any)
instance := config.Instance{
Name: getString(itemMap, "name"),
FQDN: getString(itemMap, "fqdn"),
Token: getString(itemMap, "token"),
Default: getBool(itemMap, "default"),
}
instances = append(instances, instance)
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(instances)
},
}
}
// Helper functions to safely extract values from map
func getString(m map[string]interface{}, key string) string {
if val, ok := m[key]; ok {
if str, ok := val.(string); ok {
return str
}
}
return ""
}
func getBool(m map[string]interface{}, key string) bool {
if val, ok := m[key]; ok {
if b, ok := val.(bool); ok {
return b
}
}
return false
}
+66
View File
@@ -0,0 +1,66 @@
package context
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewSetTokenCommand creates the set-token command
func NewSetDefaultCommand() *cobra.Command {
return &cobra.Command{
Use: "set-default <context_name>",
Example: `context set-default myserver`,
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Set a context as the default",
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
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, 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
}
}
if !found {
return fmt.Errorf("Context '%s' not found", name)
}
// 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
}
}
viper.Set("instances", instances)
if err := viper.WriteConfig(); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
// Show the list after updating
return NewListCommand().RunE(cmd, args)
},
}
}
+48
View File
@@ -0,0 +1,48 @@
package context
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewSetTokenCommand creates the set-token command
func NewSetTokenCommand() *cobra.Command {
return &cobra.Command{
Use: "set-token <context_name> <token>",
Example: `context set-token myserver your-new-api-token`,
Args: cli.ExactArgs(2, "<context_name> <token>"),
Short: "Update the API token for a context",
RunE: func(_ *cobra.Command, args []string) error {
name := args[0]
token := args[1]
var found interface{}
for _, instance := range viper.Get("instances").([]interface{}) {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == name {
found = instanceMap
break
}
}
if found == nil {
return fmt.Errorf("context '%s' not found", name)
}
instances := viper.Get("instances").([]interface{})
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == name {
instanceMap["token"] = token
}
}
viper.Set("instances", instances)
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
},
}
}
+91
View File
@@ -0,0 +1,91 @@
package context
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewUpdateCommand creates the update command
func NewUpdateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "update <context_name>",
Example: `context update myserver --name newname --url https://new.coolify.com --token newtoken`,
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Update a context's properties (name, URL, token)",
RunE: func(cmd *cobra.Command, args []string) error {
oldName := args[0]
instances := viper.Get("instances").([]interface{})
// Get flags
newName, _ := cmd.Flags().GetString("name")
newURL, _ := cmd.Flags().GetString("url")
newToken, _ := cmd.Flags().GetString("token")
// Check if at least one flag is provided
if newName == "" && newURL == "" && newToken == "" {
return fmt.Errorf("at least one of --name, --url, or --token must be provided")
}
// Find the context
var found bool
var contextToUpdate map[string]interface{}
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == oldName {
found = true
contextToUpdate = instanceMap
break
}
}
if !found {
return fmt.Errorf("context '%s' not found", oldName)
}
// If renaming, check if new name already exists
if newName != "" && newName != oldName {
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == newName {
return fmt.Errorf("context with name '%s' already exists", newName)
}
}
contextToUpdate["name"] = newName
}
// Update URL if provided
if newURL != "" {
contextToUpdate["fqdn"] = newURL
}
// Update token if provided
if newToken != "" {
contextToUpdate["token"] = newToken
}
// Save changes
viper.Set("instances", instances)
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
finalName := oldName
if newName != "" {
finalName = newName
}
fmt.Printf("Context '%s' updated successfully.\n", finalName)
return nil
},
}
cmd.Flags().StringP("name", "n", "", "New name for the context")
cmd.Flags().StringP("url", "u", "", "New URL for the context")
cmd.Flags().StringP("token", "t", "", "New token for the context")
return cmd
}
+67
View File
@@ -0,0 +1,67 @@
package context
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewUseCommand creates the use command
func NewUseCommand() *cobra.Command {
return &cobra.Command{
Use: "use <context_name>",
Example: `context use myserver`,
Args: cli.ExactArgs(1, "<context_name>"),
Short: "Switch to a different context (set as default)",
RunE: func(_ *cobra.Command, args []string) error {
name := args[0]
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, 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
}
}
if !found {
return fmt.Errorf("Context '%s' not found", name)
}
// Update default
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"] = true
} else {
delete(instanceMap, "default")
}
}
viper.Set("instances", instances)
if err := viper.WriteConfig(); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
fmt.Printf("Switched to context '%s'.\n", name)
return nil
},
}
}
+41
View File
@@ -0,0 +1,41 @@
package context
import (
"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)
}
+35
View File
@@ -0,0 +1,35 @@
package context
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
)
// NewVersionCommand creates the version command for contexts
func NewVersionCommand() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Get current context's Coolify version",
RunE: func(cmd *cobra.Command, _ []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)
}
// Get version using API client
version, err := client.GetVersion(ctx)
if err != nil {
return fmt.Errorf("failed to get version: %w", err)
}
fmt.Println(version)
return nil
},
}
}
+129
View File
@@ -0,0 +1,129 @@
package backup
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 creates a new database
func NewCreateCommand() *cobra.Command {
createBackupCmd := &cobra.Command{
Use: "create <database_uuid>",
Short: "Create a new scheduled backup configuration",
Long: `Create a new scheduled backup configuration for a database. Configure frequency, retention, S3 storage, and other backup options.
Example: coolify database backup create abc123 --frequency "0 0 * * *" --enabled`,
Args: cli.ExactArgs(1, "<database_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
dbUUID := args[0]
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
}
req := &models.DatabaseBackupCreateRequest{}
// Apply flags if provided
if cmd.Flags().Changed("frequency") {
frequency, _ := cmd.Flags().GetString("frequency")
req.Frequency = &frequency
}
if cmd.Flags().Changed("enabled") {
enabled, _ := cmd.Flags().GetBool("enabled")
req.Enabled = &enabled
}
if cmd.Flags().Changed("save-s3") {
saveS3, _ := cmd.Flags().GetBool("save-s3")
req.SaveS3 = &saveS3
}
if cmd.Flags().Changed("s3-storage-uuid") {
s3UUID, _ := cmd.Flags().GetString("s3-storage-uuid")
req.S3StorageUUID = &s3UUID
}
if cmd.Flags().Changed("databases") {
databases, _ := cmd.Flags().GetString("databases")
req.DatabasesToBackup = &databases
}
if cmd.Flags().Changed("dump-all") {
dumpAll, _ := cmd.Flags().GetBool("dump-all")
req.DumpAll = &dumpAll
}
if cmd.Flags().Changed("retention-amount-locally") {
amount, _ := cmd.Flags().GetInt("retention-amount-locally")
req.DatabaseBackupRetentionAmountLocally = &amount
}
if cmd.Flags().Changed("retention-days-locally") {
days, _ := cmd.Flags().GetInt("retention-days-locally")
req.DatabaseBackupRetentionDaysLocally = &days
}
if cmd.Flags().Changed("retention-storage-locally") {
storage, _ := cmd.Flags().GetString("retention-storage-locally")
req.DatabaseBackupRetentionMaxStorageLocally = &storage
}
if cmd.Flags().Changed("retention-amount-s3") {
amount, _ := cmd.Flags().GetInt("retention-amount-s3")
req.DatabaseBackupRetentionAmountS3 = &amount
}
if cmd.Flags().Changed("retention-days-s3") {
days, _ := cmd.Flags().GetInt("retention-days-s3")
req.DatabaseBackupRetentionDaysS3 = &days
}
if cmd.Flags().Changed("retention-storage-s3") {
storage, _ := cmd.Flags().GetString("retention-storage-s3")
req.DatabaseBackupRetentionMaxStorageS3 = &storage
}
if cmd.Flags().Changed("timeout") {
timeout, _ := cmd.Flags().GetInt("timeout")
req.Timeout = &timeout
}
if cmd.Flags().Changed("disable-local") {
disableLocal, _ := cmd.Flags().GetBool("disable-local")
req.DisableLocalBackup = &disableLocal
}
dbService := service.NewDatabaseService(client)
backup, err := dbService.CreateBackup(ctx, dbUUID, req)
if err != nil {
return fmt.Errorf("failed to create backup: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(backup)
},
}
createBackupCmd.Flags().String("frequency", "", "Backup frequency (cron expression, e.g., '0 0 * * *' for daily)")
createBackupCmd.Flags().Bool("enabled", false, "Enable backup schedule")
createBackupCmd.Flags().Bool("save-s3", false, "Save backups to S3")
createBackupCmd.Flags().String("s3-storage-uuid", "", "S3 storage UUID")
createBackupCmd.Flags().String("databases-to-backup", "", "Comma-separated list of databases to backup")
createBackupCmd.Flags().Bool("dump-all", false, "Dump all databases")
createBackupCmd.Flags().Int("retention-amount-locally", 0, "Number of backups to retain locally")
createBackupCmd.Flags().Int("retention-days-locally", 0, "Days to retain backups locally")
createBackupCmd.Flags().String("retention-max-storage-locally", "", "Max storage for local backups (e.g., '1GB', '500MB')")
createBackupCmd.Flags().Int("retention-amount-s3", 0, "Number of backups to retain in S3")
createBackupCmd.Flags().Int("retention-days-s3", 0, "Days to retain backups in S3")
createBackupCmd.Flags().String("retention-max-storage-s3", "", "Max storage for S3 backups (e.g., '1GB', '500MB')")
createBackupCmd.Flags().Int("timeout", 0, "Backup timeout in seconds")
createBackupCmd.Flags().Bool("disable-local-backup", false, "Disable local backup storage")
return createBackupCmd
}
+63
View File
@@ -0,0 +1,63 @@
package backup
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewDeleteExecutionCommand lists all databases
func NewDeleteExecutionCommand() *cobra.Command {
deleteBackupExecutionCmd := &cobra.Command{
Use: "delete-execution <database_uuid> <backup_uuid> <execution_uuid>",
Short: "Delete backup execution",
Long: `Delete a specific backup execution and optionally from S3. First UUID is the database, second is the backup configuration, third is the specific execution.`,
Args: cli.ExactArgs(3, "<database_uuid> <backup_uuid> <execution_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
dbUUID := args[0]
backupUUID := args[1]
executionUUID := args[2]
force, _ := cmd.Flags().GetBool("force")
deleteS3, _ := cmd.Flags().GetBool("delete-s3")
if !force {
fmt.Printf("Are you sure you want to delete backup execution %s? (y/N): ", executionUUID)
reader := bufio.NewReader(os.Stdin)
response, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("error reading input: %w", err)
}
response = strings.TrimSpace(strings.ToLower(response))
if response != "y" && response != "yes" {
fmt.Println("Delete cancelled")
return nil
}
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
err = dbService.DeleteBackupExecution(ctx, dbUUID, backupUUID, executionUUID, deleteS3)
if err != nil {
return fmt.Errorf("failed to delete backup execution: %w", err)
}
fmt.Println("Backup execution deleted successfully")
return nil
},
}
deleteBackupExecutionCmd.Flags().Bool("delete-s3", false, "Delete backup file from S3")
return deleteBackupExecutionCmd
}
+62
View File
@@ -0,0 +1,62 @@
package backup
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewDeleteCommand deletes a database
func NewDeleteCommand() *cobra.Command {
deleteBackupCmd := &cobra.Command{
Use: "delete <database_uuid> <backup_uuid>",
Short: "Delete backup configuration",
Long: `Delete a backup configuration and optionally all its executions from S3. First UUID is the database, second is the specific backup configuration.`,
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
dbUUID := args[0]
backupUUID := args[1]
force, _ := cmd.Flags().GetBool("force")
deleteS3, _ := cmd.Flags().GetBool("delete-s3")
if !force {
fmt.Printf("Are you sure you want to delete backup configuration %s? (y/N): ", backupUUID)
reader := bufio.NewReader(os.Stdin)
response, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("error reading input: %w", err)
}
response = strings.TrimSpace(strings.ToLower(response))
if response != "y" && response != "yes" {
fmt.Println("Delete cancelled")
return nil
}
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
err = dbService.DeleteBackup(ctx, dbUUID, backupUUID, deleteS3)
if err != nil {
return fmt.Errorf("failed to delete backup: %w", err)
}
fmt.Println("Backup configuration deleted successfully")
return nil
},
}
deleteBackupCmd.Flags().Bool("delete-s3", false, "Delete backup files from S3")
return deleteBackupCmd
}
+45
View File
@@ -0,0 +1,45 @@
package backup
import (
"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"
)
// NewExecutionCommand lists all databases
func NewExecutionCommand() *cobra.Command {
return &cobra.Command{
Use: "executions <database_uuid> <backup_uuid>",
Short: "List backup executions",
Long: `List all executions for a backup configuration. First UUID is the database, second is the specific backup configuration.`,
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
dbUUID := args[0]
backupUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
executions, err := dbService.ListBackupExecutions(ctx, dbUUID, backupUUID)
if err != nil {
return fmt.Errorf("failed to list backup executions: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(executions)
},
}
}
+44
View File
@@ -0,0 +1,44 @@
package backup
import (
"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 lists all databases
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list <database_uuid>",
Short: "List all backup configurations for a database",
Long: `List all backup configurations for a specific database.`,
Args: cli.ExactArgs(1, "<database_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
dbUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
backups, err := dbService.ListBackups(ctx, dbUUID)
if err != nil {
return fmt.Errorf("failed to list backups: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(backups)
},
}
}
+46
View File
@@ -0,0 +1,46 @@
package backup
import (
"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"
)
// NewTriggerCommand triggers a database backup
func NewTriggerCommand() *cobra.Command {
return &cobra.Command{
Use: "trigger <database_uuid> <backup_uuid>",
Short: "Trigger immediate backup",
Long: `Trigger an immediate backup for a specific backup configuration. First UUID is the database, second is the specific backup configuration to trigger.`,
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
dbUUID := args[0]
backupUUID := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
// Trigger immediate backup by updating with backup_now flag
req := &models.DatabaseBackupUpdateRequest{
BackupNow: cli.BoolPtr(true),
}
err = dbService.UpdateBackup(ctx, dbUUID, backupUUID, req)
if err != nil {
return fmt.Errorf("failed to trigger backup: %w", err)
}
fmt.Println("Immediate backup triggered successfully")
return nil
},
}
}
+125
View File
@@ -0,0 +1,125 @@
package backup
import (
"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 updates a database
func NewUpdateCommand() *cobra.Command {
updateBackupCmd := &cobra.Command{
Use: "update <database_uuid> <backup_uuid>",
Short: "Update backup configuration",
Long: `Update a backup configuration settings (frequency, retention, S3, etc.). First UUID is the database, second is the specific backup configuration.`,
Args: cli.ExactArgs(2, "<database_uuid> <backup_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
dbUUID := args[0]
backupUUID := args[1]
req := &models.DatabaseBackupUpdateRequest{}
hasChanges := false
if cmd.Flags().Changed("enabled") {
enabled, _ := cmd.Flags().GetBool("enabled")
req.Enabled = &enabled
hasChanges = true
}
if cmd.Flags().Changed("frequency") {
freq, _ := cmd.Flags().GetString("frequency")
req.Frequency = &freq
hasChanges = true
}
if cmd.Flags().Changed("save-s3") {
saveS3, _ := cmd.Flags().GetBool("save-s3")
req.SaveS3 = &saveS3
hasChanges = true
}
if cmd.Flags().Changed("s3-storage-uuid") {
s3UUID, _ := cmd.Flags().GetString("s3-storage-uuid")
req.S3StorageUUID = &s3UUID
hasChanges = true
}
if cmd.Flags().Changed("databases-to-backup") {
dbs, _ := cmd.Flags().GetString("databases-to-backup")
req.DatabasesToBackup = &dbs
hasChanges = true
}
if cmd.Flags().Changed("dump-all") {
dumpAll, _ := cmd.Flags().GetBool("dump-all")
req.DumpAll = &dumpAll
hasChanges = true
}
// Retention settings
if cmd.Flags().Changed("retention-amount-locally") {
amount, _ := cmd.Flags().GetInt("retention-amount-locally")
req.DatabaseBackupRetentionAmountLocally = &amount
hasChanges = true
}
if cmd.Flags().Changed("retention-days-locally") {
days, _ := cmd.Flags().GetInt("retention-days-locally")
req.DatabaseBackupRetentionDaysLocally = &days
hasChanges = true
}
if cmd.Flags().Changed("retention-max-storage-locally") {
storage, _ := cmd.Flags().GetInt("retention-max-storage-locally")
req.DatabaseBackupRetentionMaxStorageLocally = &storage
hasChanges = true
}
if cmd.Flags().Changed("retention-amount-s3") {
amount, _ := cmd.Flags().GetInt("retention-amount-s3")
req.DatabaseBackupRetentionAmountS3 = &amount
hasChanges = true
}
if cmd.Flags().Changed("retention-days-s3") {
days, _ := cmd.Flags().GetInt("retention-days-s3")
req.DatabaseBackupRetentionDaysS3 = &days
hasChanges = true
}
if cmd.Flags().Changed("retention-max-storage-s3") {
storage, _ := cmd.Flags().GetInt("retention-max-storage-s3")
req.DatabaseBackupRetentionMaxStorageS3 = &storage
hasChanges = true
}
if !hasChanges {
return fmt.Errorf("no fields to update")
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
err = dbService.UpdateBackup(ctx, dbUUID, backupUUID, req)
if err != nil {
return fmt.Errorf("failed to update backup: %w", err)
}
fmt.Println("Backup configuration updated successfully")
return nil
},
}
updateBackupCmd.Flags().Bool("enabled", false, "Enable or disable backup")
updateBackupCmd.Flags().String("frequency", "", "Backup frequency (cron expression)")
updateBackupCmd.Flags().Bool("save-s3", false, "Save backups to S3")
updateBackupCmd.Flags().String("s3-storage-uuid", "", "S3 storage UUID")
updateBackupCmd.Flags().String("databases-to-backup", "", "Comma-separated list of databases to backup")
updateBackupCmd.Flags().Bool("dump-all", false, "Dump all databases")
updateBackupCmd.Flags().Int("retention-amount-locally", 0, "Number of backups to retain locally")
updateBackupCmd.Flags().Int("retention-days-locally", 0, "Days to retain backups locally")
updateBackupCmd.Flags().Int("retention-max-storage-locally", 0, "Max storage for local backups (MB)")
updateBackupCmd.Flags().Int("retention-amount-s3", 0, "Number of backups to retain in S3")
updateBackupCmd.Flags().Int("retention-days-s3", 0, "Days to retain backups in S3")
updateBackupCmd.Flags().Int("retention-max-storage-s3", 0, "Max storage for S3 backups (MB)")
return updateBackupCmd
}
+287
View File
@@ -0,0 +1,287 @@
package database
import (
"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"
)
func NewCreateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create <type>",
Short: "Create a new database",
Long: `Create a new database of the specified type.
Supported types: postgresql, mysql, mariadb, mongodb, redis, keydb, clickhouse, dragonfly
Examples:
coolify databases create postgresql --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production
coolify databases create mysql --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production --name="My MySQL"`,
Args: cli.ExactArgs(1, "<type>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
dbType := args[0]
validTypes := []string{"postgresql", "mysql", "mariadb", "mongodb", "redis", "keydb", "clickhouse", "dragonfly"}
isValid := false
for _, t := range validTypes {
if t == dbType {
isValid = true
break
}
}
if !isValid {
return fmt.Errorf("invalid database type '%s'. Valid types: %s", dbType, strings.Join(validTypes, ", "))
}
serverUUID, _ := cmd.Flags().GetString("server-uuid")
projectUUID, _ := cmd.Flags().GetString("project-uuid")
environmentName, _ := cmd.Flags().GetString("environment-name")
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
if serverUUID == "" || projectUUID == "" {
return fmt.Errorf("--server-uuid and --project-uuid are required")
}
if environmentName == "" && environmentUUID == "" {
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
}
req := &models.DatabaseCreateRequest{
ServerUUID: serverUUID,
ProjectUUID: projectUUID,
}
if environmentName != "" {
req.EnvironmentName = &environmentName
}
if environmentUUID != "" {
req.EnvironmentUUID = &environmentUUID
}
// Common flags
if cmd.Flags().Changed("name") {
name, _ := cmd.Flags().GetString("name")
req.Name = &name
}
if cmd.Flags().Changed("description") {
desc, _ := cmd.Flags().GetString("description")
req.Description = &desc
}
if cmd.Flags().Changed("image") {
image, _ := cmd.Flags().GetString("image")
req.Image = &image
}
if cmd.Flags().Changed("destination-uuid") {
dest, _ := cmd.Flags().GetString("destination-uuid")
req.DestinationUUID = &dest
}
if cmd.Flags().Changed("instant-deploy") {
instant, _ := cmd.Flags().GetBool("instant-deploy")
req.InstantDeploy = &instant
}
if cmd.Flags().Changed("is-public") {
isPublic, _ := cmd.Flags().GetBool("is-public")
req.IsPublic = &isPublic
}
if cmd.Flags().Changed("public-port") {
port, _ := cmd.Flags().GetInt("public-port")
req.PublicPort = &port
}
// Resource limits
if cmd.Flags().Changed("limits-memory") {
mem, _ := cmd.Flags().GetString("limits-memory")
req.LimitsMemory = &mem
}
if cmd.Flags().Changed("limits-cpus") {
cpus, _ := cmd.Flags().GetString("limits-cpus")
req.LimitsCpus = &cpus
}
// PostgreSQL specific
if dbType == "postgresql" {
if cmd.Flags().Changed("postgres-user") {
user, _ := cmd.Flags().GetString("postgres-user")
req.PostgresUser = &user
}
if cmd.Flags().Changed("postgres-password") {
pass, _ := cmd.Flags().GetString("postgres-password")
req.PostgresPassword = &pass
}
if cmd.Flags().Changed("postgres-db") {
db, _ := cmd.Flags().GetString("postgres-db")
req.PostgresDB = &db
}
}
// MySQL specific
if dbType == "mysql" {
if cmd.Flags().Changed("mysql-root-password") {
pass, _ := cmd.Flags().GetString("mysql-root-password")
req.MysqlRootPassword = &pass
}
if cmd.Flags().Changed("mysql-user") {
user, _ := cmd.Flags().GetString("mysql-user")
req.MysqlUser = &user
}
if cmd.Flags().Changed("mysql-password") {
pass, _ := cmd.Flags().GetString("mysql-password")
req.MysqlPassword = &pass
}
if cmd.Flags().Changed("mysql-database") {
db, _ := cmd.Flags().GetString("mysql-database")
req.MysqlDatabase = &db
}
}
// MariaDB specific
if dbType == "mariadb" {
if cmd.Flags().Changed("mariadb-root-password") {
pass, _ := cmd.Flags().GetString("mariadb-root-password")
req.MariadbRootPassword = &pass
}
if cmd.Flags().Changed("mariadb-user") {
user, _ := cmd.Flags().GetString("mariadb-user")
req.MariadbUser = &user
}
if cmd.Flags().Changed("mariadb-password") {
pass, _ := cmd.Flags().GetString("mariadb-password")
req.MariadbPassword = &pass
}
if cmd.Flags().Changed("mariadb-database") {
db, _ := cmd.Flags().GetString("mariadb-database")
req.MariadbDatabase = &db
}
}
// MongoDB specific
if dbType == "mongodb" {
if cmd.Flags().Changed("mongo-root-username") {
user, _ := cmd.Flags().GetString("mongo-root-username")
req.MongoInitdbRootUsername = &user
}
if cmd.Flags().Changed("mongo-root-password") {
pass, _ := cmd.Flags().GetString("mongo-root-password")
req.MongoInitdbRootPassword = &pass
}
if cmd.Flags().Changed("mongo-database") {
db, _ := cmd.Flags().GetString("mongo-database")
req.MongoInitdbDatabase = &db
}
}
// Redis specific
if dbType == "redis" {
if cmd.Flags().Changed("redis-password") {
pass, _ := cmd.Flags().GetString("redis-password")
req.RedisPassword = &pass
}
}
// KeyDB specific
if dbType == "keydb" {
if cmd.Flags().Changed("keydb-password") {
pass, _ := cmd.Flags().GetString("keydb-password")
req.KeydbPassword = &pass
}
}
// Clickhouse specific
if dbType == "clickhouse" {
if cmd.Flags().Changed("clickhouse-admin-user") {
user, _ := cmd.Flags().GetString("clickhouse-admin-user")
req.ClickhouseAdminUser = &user
}
if cmd.Flags().Changed("clickhouse-admin-password") {
pass, _ := cmd.Flags().GetString("clickhouse-admin-password")
req.ClickhouseAdminPassword = &pass
}
}
// Dragonfly specific
if dbType == "dragonfly" {
if cmd.Flags().Changed("dragonfly-password") {
pass, _ := cmd.Flags().GetString("dragonfly-password")
req.DragonflyPassword = &pass
}
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
database, err := dbService.Create(ctx, dbType, req)
if err != nil {
return fmt.Errorf("failed to create database: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(database)
},
}
// Common flags
cmd.Flags().String("server-uuid", "", "Server UUID (required)")
cmd.Flags().String("project-uuid", "", "Project UUID (required)")
cmd.Flags().String("environment-name", "", "Environment name")
cmd.Flags().String("environment-uuid", "", "Environment UUID")
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
cmd.Flags().String("name", "", "Database name")
cmd.Flags().String("description", "", "Database description")
cmd.Flags().String("image", "", "Docker image")
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
cmd.Flags().Bool("is-public", false, "Make database publicly accessible")
cmd.Flags().Int("public-port", 0, "Public port")
cmd.Flags().String("limits-memory", "", "Memory limit (e.g., '512m', '2g')")
cmd.Flags().String("limits-cpus", "", "CPU limit (e.g., '0.5', '2')")
// PostgreSQL flags
cmd.Flags().String("postgres-user", "", "PostgreSQL user")
cmd.Flags().String("postgres-password", "", "PostgreSQL password")
cmd.Flags().String("postgres-db", "", "PostgreSQL database name")
// MySQL flags
cmd.Flags().String("mysql-root-password", "", "MySQL root password")
cmd.Flags().String("mysql-user", "", "MySQL user")
cmd.Flags().String("mysql-password", "", "MySQL password")
cmd.Flags().String("mysql-database", "", "MySQL database name")
// MariaDB flags
cmd.Flags().String("mariadb-root-password", "", "MariaDB root password")
cmd.Flags().String("mariadb-user", "", "MariaDB user")
cmd.Flags().String("mariadb-password", "", "MariaDB password")
cmd.Flags().String("mariadb-database", "", "MariaDB database name")
// MongoDB flags
cmd.Flags().String("mongo-root-username", "", "MongoDB root username")
cmd.Flags().String("mongo-root-password", "", "MongoDB root password")
cmd.Flags().String("mongo-database", "", "MongoDB database name")
// Redis flags
cmd.Flags().String("redis-password", "", "Redis password")
// KeyDB flags
cmd.Flags().String("keydb-password", "", "KeyDB password")
// Clickhouse flags
cmd.Flags().String("clickhouse-admin-user", "", "Clickhouse admin user")
cmd.Flags().String("clickhouse-admin-password", "", "Clickhouse admin password")
// Dragonfly flags
cmd.Flags().String("dragonfly-password", "", "Dragonfly password")
return cmd
}
+43
View File
@@ -0,0 +1,43 @@
package database
import (
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/cmd/database/backup"
)
// NewDatabaseCommand creates the database parent command with all subcommands
func NewDatabaseCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "database",
Aliases: []string{"databases", "db", "dbs"},
Short: "Manage Coolify databases",
Long: `Manage Coolify databases (PostgreSQL, MySQL, MongoDB, Redis, MariaDB, KeyDB, Clickhouse, Dragonfly).`,
}
// Add main database commands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewStartCommand())
cmd.AddCommand(NewStopCommand())
cmd.AddCommand(NewRestartCommand())
cmd.AddCommand(NewCreateCommand())
cmd.AddCommand(NewUpdateCommand())
cmd.AddCommand(NewDeleteCommand())
// Add backup subcommand
backupCmd := &cobra.Command{
Use: "backup",
Short: "Manage database backups",
}
backupCmd.AddCommand(backup.NewCreateCommand())
backupCmd.AddCommand(backup.NewListCommand())
backupCmd.AddCommand(backup.NewDeleteCommand())
backupCmd.AddCommand(backup.NewUpdateCommand())
backupCmd.AddCommand(backup.NewTriggerCommand())
backupCmd.AddCommand(backup.NewExecutionCommand())
backupCmd.AddCommand(backup.NewDeleteExecutionCommand())
cmd.AddCommand(backupCmd)
return cmd
}
+68
View File
@@ -0,0 +1,68 @@
package database
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewDeleteCommand deletes a database
func NewDeleteCommand() *cobra.Command {
deleteDatabaseCmd := &cobra.Command{
Use: "delete <uuid>",
Short: "Delete a database",
Long: `Delete a database and optionally clean up its configurations, volumes, and networks.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
force, _ := cmd.Flags().GetBool("force")
deleteConfigurations, _ := cmd.Flags().GetBool("delete-configurations")
deleteVolumes, _ := cmd.Flags().GetBool("delete-volumes")
dockerCleanup, _ := cmd.Flags().GetBool("docker-cleanup")
deleteConnectedNetworks, _ := cmd.Flags().GetBool("delete-connected-networks")
if !force {
fmt.Printf("Are you sure you want to delete database %s? (y/N): ", uuid)
reader := bufio.NewReader(os.Stdin)
response, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("error reading input: %w", err)
}
response = strings.TrimSpace(strings.ToLower(response))
if response != "y" && response != "yes" {
fmt.Println("Delete cancelled")
return nil
}
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
err = dbService.Delete(ctx, uuid, deleteConfigurations, deleteVolumes, dockerCleanup, deleteConnectedNetworks)
if err != nil {
return fmt.Errorf("failed to delete database: %w", err)
}
fmt.Println("Database deleted successfully")
return nil
},
}
deleteDatabaseCmd.Flags().Bool("delete-configurations", true, "Delete configurations")
deleteDatabaseCmd.Flags().Bool("delete-volumes", true, "Delete volumes")
deleteDatabaseCmd.Flags().Bool("docker-cleanup", true, "Run docker cleanup")
deleteDatabaseCmd.Flags().Bool("delete-connected-networks", true, "Delete connected networks")
return deleteDatabaseCmd
}
+46
View File
@@ -0,0 +1,46 @@
package database
import (
"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"
)
// NewGetCommand gets database details
func NewGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <uuid>",
Short: "Get database details",
Long: `Get detailed information about a specific database by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
database, err := dbService.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get database: %w", err)
}
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter("table", output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(database)
},
}
}
+41
View File
@@ -0,0 +1,41 @@
package database
import (
"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 lists all databases
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all databases",
Long: `List all databases in Coolify.`,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
databases, err := dbService.List(ctx)
if err != nil {
return fmt.Errorf("failed to list databases: %w", err)
}
formatter, err := output.NewFormatter("table", output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(databases)
},
}
}
+38
View File
@@ -0,0 +1,38 @@
package database
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewRestartCommand restarts a database
func NewRestartCommand() *cobra.Command {
return &cobra.Command{
Use: "restart <uuid>",
Short: "Restart a database",
Long: `Restart a database by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
response, err := dbService.Restart(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to restart database: %w", err)
}
fmt.Println(response.Message)
return nil
},
}
}
+38
View File
@@ -0,0 +1,38 @@
package database
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewStartCommand starts a database
func NewStartCommand() *cobra.Command {
return &cobra.Command{
Use: "start <uuid>",
Short: "Start a database",
Long: `Start a database by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
response, err := dbService.Start(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to start database: %w", err)
}
fmt.Println(response.Message)
return nil
},
}
}
+38
View File
@@ -0,0 +1,38 @@
package database
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewStopCommand stops a database
func NewStopCommand() *cobra.Command {
return &cobra.Command{
Use: "stop <uuid>",
Short: "Stop a database",
Long: `Stop a database by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
response, err := dbService.Stop(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to stop database: %w", err)
}
fmt.Println(response.Message)
return nil
},
}
}
+116
View File
@@ -0,0 +1,116 @@
package database
import (
"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 updates a database
func NewUpdateCommand() *cobra.Command {
updateDatabaseCmd := &cobra.Command{
Use: "update <uuid>",
Short: "Update a database",
Long: `Update a database's configuration by UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
req := &models.DatabaseUpdateRequest{}
hasChanges := false
if cmd.Flags().Changed("name") {
name, _ := cmd.Flags().GetString("name")
req.Name = &name
hasChanges = true
}
if cmd.Flags().Changed("description") {
desc, _ := cmd.Flags().GetString("description")
req.Description = &desc
hasChanges = true
}
if cmd.Flags().Changed("image") {
image, _ := cmd.Flags().GetString("image")
req.Image = &image
hasChanges = true
}
if cmd.Flags().Changed("is-public") {
isPublic, _ := cmd.Flags().GetBool("is-public")
req.IsPublic = &isPublic
hasChanges = true
}
if cmd.Flags().Changed("public-port") {
port, _ := cmd.Flags().GetInt("public-port")
req.PublicPort = &port
hasChanges = true
}
// Resource limits
if cmd.Flags().Changed("limits-memory") {
mem, _ := cmd.Flags().GetString("limits-memory")
req.LimitsMemory = &mem
hasChanges = true
}
if cmd.Flags().Changed("limits-cpus") {
cpus, _ := cmd.Flags().GetString("limits-cpus")
req.LimitsCpus = &cpus
hasChanges = true
}
if !hasChanges {
return fmt.Errorf("no fields to update")
}
// Validate is-public requires public-port
if req.IsPublic != nil && *req.IsPublic {
// If setting to public, check if port is provided or fetch current database to check existing port
if req.PublicPort == nil || *req.PublicPort == 0 {
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
currentDB, err := dbService.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get current database: %w", err)
}
// Check if database already has a public port
if currentDB.PublicPort == nil || *currentDB.PublicPort == 0 {
return fmt.Errorf("cannot set database as public without a public port. Please provide --public-port")
}
}
}
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
dbService := service.NewDatabaseService(client)
err = dbService.Update(ctx, uuid, req)
if err != nil {
return fmt.Errorf("failed to update database: %w", err)
}
fmt.Println("Database updated successfully")
return nil
},
}
updateDatabaseCmd.Flags().String("name", "", "Database name")
updateDatabaseCmd.Flags().String("description", "", "Database description")
updateDatabaseCmd.Flags().String("image", "", "Docker image")
updateDatabaseCmd.Flags().Bool("is-public", false, "Make database publicly accessible")
updateDatabaseCmd.Flags().Int("public-port", 0, "Public port")
updateDatabaseCmd.Flags().String("limits-memory", "", "Memory limit")
updateDatabaseCmd.Flags().String("limits-cpus", "", "CPU limit")
return updateDatabaseCmd
}
-79
View File
@@ -1,79 +0,0 @@
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"log"
"github.com/spf13/cobra"
)
type Deploy struct {
Deployments []Deployment `json:"deployments"`
}
type Deployment struct {
Message string `json:"message"`
ResourceUuid string `json:"resource_uuid"`
DeploymentUuid string `json:"deployment_uuid"`
}
var deployCmd = &cobra.Command{
Use: "deploy",
Short: "Deploy related commands",
}
var deployByUuidCmd = &cobra.Command{
Use: "uuid <uuid>",
Short: "Deploy by uuid",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
var CsvUuids = ""
for _, uuid := range args {
CsvUuids += uuid + ","
}
CsvUuids = CsvUuids[:len(CsvUuids)-1]
data, err := Fetch("deploy?uuid=" + CsvUuids)
if err != nil {
log.Println(err)
return
}
if PrettyMode {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(prettyJSON.String()))
return
}
if JsonMode {
fmt.Println(data)
return
}
var jsondata Deploy
err = json.Unmarshal([]byte(data), &jsondata)
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintln(w, "Message\tResource Uuid\tDeployment Uuid")
for _, resource := range jsondata.Deployments {
fmt.Fprintf(w, "%s\t%s\t%s\n", resource.Message, resource.ResourceUuid, resource.DeploymentUuid)
}
w.Flush()
},
}
// TODO deployByTagCmd
func init() {
rootCmd.AddCommand(deployCmd)
deployCmd.AddCommand(deployByUuidCmd)
}
+131
View File
@@ -0,0 +1,131 @@
package deployment
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewBatchCommand deploys multiple resources by name
func NewBatchCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "batch <name1,name2,...>",
Short: "Deploy multiple resources by name",
Long: `Deploy multiple resources at once.
Provide resource names as comma-separated values.
Example: coolify deploy batch app1,app2,app3`,
Args: cli.ExactArgs(1, "<names>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
namesStr := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Parse comma-separated names
names := make([]string, 0)
for _, name := range strings.Split(namesStr, ",") {
name = strings.TrimSpace(name)
if name != "" {
names = append(names, name)
}
}
if len(names) == 0 {
return fmt.Errorf("no resource names provided")
}
// Find resources by name
resourceSvc := service.NewResourceService(client)
resources, err := resourceSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list resources: %w", err)
}
// Build map of name -> UUID
nameToUUID := make(map[string]string)
for _, r := range resources {
nameToUUID[r.Name] = r.UUID
}
// Validate all names exist
var notFound []string
for _, name := range names {
if _, exists := nameToUUID[name]; !exists {
notFound = append(notFound, name)
}
}
if len(notFound) > 0 {
return fmt.Errorf("resources not found: %v", notFound)
}
// Deploy all resources
force, _ := cmd.Flags().GetBool("force")
deploySvc := service.NewDeploymentService(client)
type result struct {
Name string
UUID string
Success bool
Message string
Error string
}
results := make([]result, 0, len(names))
for _, name := range names {
uuid := nameToUUID[name]
fmt.Printf("Deploying %s...\n", name)
res, err := deploySvc.Deploy(ctx, uuid, force)
if err != nil {
results = append(results, result{
Name: name,
UUID: uuid,
Success: false,
Error: err.Error(),
})
fmt.Printf(" ❌ Failed: %v\n", err)
} else {
// Get first deployment message from the array
message := ""
if len(res.Deployments) > 0 {
message = res.Deployments[0].Message
}
results = append(results, result{
Name: name,
UUID: uuid,
Success: true,
Message: message,
})
fmt.Printf(" ✅ Success: %s\n", message)
}
}
// Summary
successCount := 0
for _, r := range results {
if r.Success {
successCount++
}
}
fmt.Printf("\nBatch deployment complete: %d/%d succeeded\n", successCount, len(results))
if successCount < len(results) {
return fmt.Errorf("some deployments failed")
}
return nil
},
}
cmd.Flags().Bool("force", false, "Force deployment")
return cmd
}
+75
View File
@@ -0,0 +1,75 @@
package deployment
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"
)
// NewCancelCommand cancels a deployment
func NewCancelCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "cancel <uuid>",
Short: "Cancel a deployment by UUID",
Long: `Cancel an in-progress deployment. This will stop the deployment process and clean up any temporary resources.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
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
}
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 {
fmt.Printf("Are you sure you want to cancel deployment %s? (yes/no): ", uuid)
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.")
return nil
}
}
deploySvc := service.NewDeploymentService(client)
result, err := deploySvc.Cancel(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to cancel deployment: %w", err)
}
format, 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)
}
return formatter.Format(result)
},
}
cmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
return cmd
}
+21
View File
@@ -0,0 +1,21 @@
package deployment
import "github.com/spf13/cobra"
// NewDeploymentCommand creates the deployment parent command with all subcommands
func NewDeploymentCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "deploy",
Short: "Deploy related commands",
}
// Add all deployment subcommands
cmd.AddCommand(NewUUIDCommand())
cmd.AddCommand(NewNameCommand())
cmd.AddCommand(NewBatchCommand())
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewCancelCommand())
return cmd
}
+44
View File
@@ -0,0 +1,44 @@
package deployment
import (
"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"
)
// NewGetCommand gets deployment details
func NewGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <uuid>",
Short: "Get deployment details by UUID",
Long: `Get detailed information about a specific deployment by its UUID.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
deploySvc := service.NewDeploymentService(client)
deployment, err := deploySvc.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get deployment: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(deployment)
},
}
}
+42
View File
@@ -0,0 +1,42 @@
package deployment
import (
"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 lists all deployments
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all deployments",
Long: `List all currently running deployments across all resources.`,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
deploySvc := service.NewDeploymentService(client)
deployments, err := deploySvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list deployments: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return fmt.Errorf("failed to create formatter: %w", err)
}
return formatter.Format(deployments)
},
}
}
+79
View File
@@ -0,0 +1,79 @@
package deployment
import (
"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"
)
// NewNameCommand deploys a resource by name
func NewNameCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "name <resource_name>",
Short: "Deploy by resource name",
Args: cli.ExactArgs(1, "<resource_name>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
name := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Find resource by name
resourceSvc := service.NewResourceService(client)
resources, err := resourceSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list resources: %w", err)
}
var matchedUUID string
for _, r := range resources {
if r.Name == name {
matchedUUID = r.UUID
break
}
}
if matchedUUID == "" {
return fmt.Errorf("resource with name '%s' not found", name)
}
// Deploy using the found UUID
force, _ := cmd.Flags().GetBool("force")
deploySvc := service.NewDeploymentService(client)
result, err := deploySvc.Deploy(ctx, matchedUUID, force)
if err != nil {
return fmt.Errorf("failed to deploy resource: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
// For table format, convert deployment info array to display format
if format == output.FormatTable {
displays := make([]ResultDisplay, len(result.Deployments))
for i, dep := range result.Deployments {
displays[i] = ResultDisplay{
Message: dep.Message,
DeploymentUUID: dep.DeploymentUUID,
}
}
return formatter.Format(displays)
}
return formatter.Format(result)
},
}
cmd.Flags().Bool("force", false, "Force deployment")
return cmd
}
+65
View File
@@ -0,0 +1,65 @@
package deployment
import (
"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"
)
// ResultDisplay represents a deploy result for table display
type ResultDisplay struct {
Message string `json:"message"`
DeploymentUUID string `json:"deployment_uuid"`
}
// NewUUIDCommand deploys a resource by UUID
func NewUUIDCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "uuid <uuid>",
Short: "Deploy by uuid",
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
deploySvc := service.NewDeploymentService(client)
result, err := deploySvc.Deploy(ctx, uuid, force)
if err != nil {
return fmt.Errorf("failed to deploy resource: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
// For table format, convert deployment info array to display format
if format == output.FormatTable {
displays := make([]ResultDisplay, len(result.Deployments))
for i, dep := range result.Deployments {
displays[i] = ResultDisplay{
Message: dep.Message,
DeploymentUUID: dep.DeploymentUUID,
}
}
return formatter.Format(displays)
}
return formatter.Format(result)
},
}
cmd.Flags().Bool("force", false, "Force deployment")
return cmd
}
+96
View File
@@ -0,0 +1,96 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
var docsCmd = &cobra.Command{
Use: "docs",
Short: "Generate documentation",
Hidden: true,
}
var manCmd = &cobra.Command{
Use: "man",
Short: "Generate man pages",
Long: `Generate man pages for all Coolify CLI commands.
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, _ []string) error {
outputDir, _ := cmd.Flags().GetString("output-dir")
// Create output directory if it doesn't exist
if err := os.MkdirAll(outputDir, 0750); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
// Generate man pages
header := &doc.GenManHeader{
Title: "COOLIFY",
Section: "1",
Source: "Coolify CLI",
}
if err := doc.GenManTree(rootCmd, header, outputDir); err != nil {
return fmt.Errorf("failed to generate man pages: %w", err)
}
absPath, _ := filepath.Abs(outputDir)
fmt.Printf("Man pages generated successfully in: %s\n", absPath)
fmt.Println("\nTo install the man pages system-wide:")
fmt.Println(" sudo cp man/*.1 /usr/local/share/man/man1/")
fmt.Println(" sudo mandb")
fmt.Println("\nTo view a man page:")
fmt.Println(" man coolify")
fmt.Println(" man coolify-servers")
return nil
},
}
var markdownCmd = &cobra.Command{
Use: "markdown",
Aliases: []string{"md"},
Short: "Generate markdown documentation",
Long: `Generate markdown documentation for all Coolify CLI commands.
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, _ []string) error {
outputDir, _ := cmd.Flags().GetString("output-dir")
// Create output directory if it doesn't exist
if err := os.MkdirAll(outputDir, 0750); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
// Generate markdown docs
if err := doc.GenMarkdownTree(rootCmd, outputDir); err != nil {
return fmt.Errorf("failed to generate markdown docs: %w", err)
}
absPath, _ := filepath.Abs(outputDir)
fmt.Printf("Markdown documentation generated successfully in: %s\n", absPath)
return nil
},
}
func NewDocsCommand() *cobra.Command {
docsCmd.AddCommand(manCmd)
docsCmd.AddCommand(markdownCmd)
manCmd.Flags().StringP("output-dir", "o", "./man", "Output directory for man pages")
markdownCmd.Flags().StringP("output-dir", "o", "./docs", "Output directory for markdown files")
return docsCmd
}
-68
View File
@@ -1,68 +0,0 @@
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"log"
"github.com/spf13/cobra"
)
type Domain struct {
IP string `json:"ip"`
Domains []string `json:"domains"`
}
var domainsCmd = &cobra.Command{
Use: "domains",
Short: "Domain related commands",
}
var listDomainsCmd = &cobra.Command{
Use: "list",
Short: "List all domains",
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
data, err := Fetch("domains")
if err != nil {
log.Println(err)
return
}
if PrettyMode {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(prettyJSON.String()))
return
}
if JsonMode {
fmt.Println(data)
return
}
var jsondata []Domain
err = json.Unmarshal([]byte(data), &jsondata)
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintln(w, "IP Address\tDomains")
for _, resource := range jsondata {
for _, domain := range resource.Domains {
fmt.Fprintf(w, "%s\t%s\n", resource.IP, domain)
}
}
w.Flush()
},
}
func init() {
// rootCmd.AddCommand(domainsCmd)
// domainsCmd.AddCommand(listDomainsCmd)
}
+53
View File
@@ -0,0 +1,53 @@
package github
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 NewListBranchesCommand() *cobra.Command {
return &cobra.Command{
Use: "branches <app_uuid> <owner/repo>",
Short: "List branches for a repository",
Long: `List all branches for a specific repository. Provide the app UUID and repository in owner/repo format.
Example: coolify github branches abc-123-def owner/repository`,
Args: cli.ExactArgs(2, "<app_uuid> <owner/repo>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
appUUID := args[0]
// Parse owner/repo
ownerRepo := args[1]
parts := cli.SplitOwnerRepo(ownerRepo)
if len(parts) != 2 {
return fmt.Errorf("invalid repository format. Expected 'owner/repo', got '%s'", ownerRepo)
}
owner, repo := parts[0], parts[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
svc := service.NewGitHubAppService(client)
branches, err := svc.ListBranches(ctx, appUUID, owner, repo)
if err != nil {
return fmt.Errorf("failed to list branches: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(branches)
},
}
}
+113
View File
@@ -0,0 +1,113 @@
package github
import (
"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"
)
func NewCreateCommand() *cobra.Command {
createCmd := &cobra.Command{
Use: "create",
Short: "Create a GitHub App integration",
Long: `Create a new GitHub App integration. This allows you to deploy private repositories from GitHub.
Required flags: --name, --api-url, --html-url, --app-id, --installation-id, --client-id, --client-secret, --private-key-uuid
Example: coolify github create --name "My GitHub App" --api-url "https://api.github.com" --html-url "https://github.com" --app-id 123456 --installation-id 789012 --client-id "Iv1.abc123" --client-secret "secret123" --private-key-uuid "abc-123-def-456"`,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
name, _ := cmd.Flags().GetString("name")
apiURL, _ := cmd.Flags().GetString("api-url")
htmlURL, _ := cmd.Flags().GetString("html-url")
appID, _ := cmd.Flags().GetInt("app-id")
installationID, _ := cmd.Flags().GetInt("installation-id")
clientID, _ := cmd.Flags().GetString("client-id")
clientSecret, _ := cmd.Flags().GetString("client-secret")
privateKeyUUID, _ := cmd.Flags().GetString("private-key-uuid")
req := &models.GitHubAppCreateRequest{
Name: name,
APIURL: apiURL,
HTMLURL: htmlURL,
AppID: appID,
InstallationID: installationID,
ClientID: clientID,
ClientSecret: clientSecret,
PrivateKeyUUID: privateKeyUUID,
}
// Optional fields
if cmd.Flags().Changed("organization") {
org, _ := cmd.Flags().GetString("organization")
req.Organization = &org
}
if cmd.Flags().Changed("custom-user") {
user, _ := cmd.Flags().GetString("custom-user")
req.CustomUser = &user
}
if cmd.Flags().Changed("custom-port") {
port, _ := cmd.Flags().GetInt("custom-port")
req.CustomPort = &port
}
if cmd.Flags().Changed("webhook-secret") {
secret, _ := cmd.Flags().GetString("webhook-secret")
req.WebhookSecret = &secret
}
if cmd.Flags().Changed("system-wide") {
systemWide, _ := cmd.Flags().GetBool("system-wide")
req.IsSystemWide = &systemWide
}
svc := service.NewGitHubAppService(client)
app, err := svc.Create(ctx, req)
if err != nil {
return fmt.Errorf("failed to create GitHub App: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(app)
},
}
createCmd.Flags().String("name", "", "GitHub App name (required)")
createCmd.Flags().String("organization", "", "GitHub organization")
createCmd.Flags().String("api-url", "", "GitHub API URL (required, e.g., https://api.github.com)")
createCmd.Flags().String("html-url", "", "GitHub HTML URL (required, e.g., https://github.com)")
createCmd.Flags().String("custom-user", "", "Custom user for SSH (default: git)")
createCmd.Flags().Int("custom-port", 0, "Custom port for SSH (default: 22)")
createCmd.Flags().Int("app-id", 0, "GitHub App ID (required)")
createCmd.Flags().Int("installation-id", 0, "GitHub Installation ID (required)")
createCmd.Flags().String("client-id", "", "GitHub OAuth Client ID (required)")
createCmd.Flags().String("client-secret", "", "GitHub OAuth Client Secret (required)")
createCmd.Flags().String("webhook-secret", "", "GitHub Webhook Secret")
createCmd.Flags().String("private-key-uuid", "", "UUID of existing private key (required)")
createCmd.Flags().Bool("system-wide", false, "Is this app system-wide (cloud only)")
_ = createCmd.MarkFlagRequired("name")
_ = createCmd.MarkFlagRequired("api-url")
_ = createCmd.MarkFlagRequired("html-url")
_ = createCmd.MarkFlagRequired("app-id")
_ = createCmd.MarkFlagRequired("installation-id")
_ = createCmd.MarkFlagRequired("client-id")
_ = createCmd.MarkFlagRequired("client-secret")
_ = createCmd.MarkFlagRequired("private-key-uuid")
return createCmd
}
+59
View File
@@ -0,0 +1,59 @@
package github
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
func NewDeleteCommand() *cobra.Command {
deleteCmd := &cobra.Command{
Use: "delete <app_uuid>",
Short: "Delete a GitHub App integration",
Long: `Delete a GitHub App integration. The app must not be used by any applications.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to delete GitHub App %s? This cannot be undone. (yes/no): ", appUUID)
_, 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.")
return nil
}
}
svc := service.NewGitHubAppService(client)
err = svc.Delete(ctx, appUUID)
if err != nil {
return fmt.Errorf("failed to delete GitHub App: %w", err)
}
fmt.Println("GitHub App deleted successfully")
return nil
},
}
deleteCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
return deleteCmd
}
+43
View File
@@ -0,0 +1,43 @@
package github
import (
"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 <app_uuid>",
Short: "Get GitHub App details by UUID",
Long: `Get detailed information about a specific GitHub App integration.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
svc := service.NewGitHubAppService(client)
app, err := svc.Get(ctx, appUUID)
if err != nil {
return fmt.Errorf("failed to get GitHub App: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(app)
},
}
}
+25
View File
@@ -0,0 +1,25 @@
package github
import (
"github.com/spf13/cobra"
)
func NewGitHubCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "github",
Aliases: []string{"gh", "github-app", "github-apps"},
Short: "Manage GitHub App integrations",
Long: `Manage GitHub App integrations for private repository deployments.`,
}
// Add main database commands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewCreateCommand())
cmd.AddCommand(NewUpdateCommand())
cmd.AddCommand(NewDeleteCommand())
cmd.AddCommand(NewListRepositoriesCommand())
cmd.AddCommand(NewListBranchesCommand())
return cmd
}
+46
View File
@@ -0,0 +1,46 @@
package github
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",
Short: "List all GitHub App integrations",
Long: `List all GitHub App integrations configured in Coolify.`,
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 {
return fmt.Errorf("failed to list GitHub Apps: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(apps)
},
}
}
+43
View File
@@ -0,0 +1,43 @@
package github
import (
"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 NewListRepositoriesCommand() *cobra.Command {
return &cobra.Command{
Use: "repos <app_uuid>",
Short: "List repositories accessible by a GitHub App",
Long: `List all repositories that are accessible by the specified GitHub App.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
svc := service.NewGitHubAppService(client)
repos, err := svc.ListRepositories(ctx, appUUID)
if err != nil {
return fmt.Errorf("failed to list repositories: %w", err)
}
format, _ := cmd.Flags().GetString("format")
formatter, err := output.NewFormatter(format, output.Options{})
if err != nil {
return err
}
return formatter.Format(repos)
},
}
}
+110
View File
@@ -0,0 +1,110 @@
package github
import (
"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 {
updateCmd := &cobra.Command{
Use: "update <app_uuid>",
Short: "Update a GitHub App integration",
Long: `Update an existing GitHub App integration. Provide the app UUID and the fields you want to update.`,
Args: cli.ExactArgs(1, "<app_uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
appUUID := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
req := &models.GitHubAppUpdateRequest{}
// Update only fields that were explicitly provided
if cmd.Flags().Changed("name") {
name, _ := cmd.Flags().GetString("name")
req.Name = &name
}
if cmd.Flags().Changed("organization") {
org, _ := cmd.Flags().GetString("organization")
req.Organization = &org
}
if cmd.Flags().Changed("api-url") {
apiURL, _ := cmd.Flags().GetString("api-url")
req.APIURL = &apiURL
}
if cmd.Flags().Changed("html-url") {
htmlURL, _ := cmd.Flags().GetString("html-url")
req.HTMLURL = &htmlURL
}
if cmd.Flags().Changed("custom-user") {
user, _ := cmd.Flags().GetString("custom-user")
req.CustomUser = &user
}
if cmd.Flags().Changed("custom-port") {
port, _ := cmd.Flags().GetInt("custom-port")
req.CustomPort = &port
}
if cmd.Flags().Changed("app-id") {
id, _ := cmd.Flags().GetInt("app-id")
req.AppID = &id
}
if cmd.Flags().Changed("installation-id") {
id, _ := cmd.Flags().GetInt("installation-id")
req.InstallationID = &id
}
if cmd.Flags().Changed("client-id") {
clientID, _ := cmd.Flags().GetString("client-id")
req.ClientID = &clientID
}
if cmd.Flags().Changed("client-secret") {
clientSecret, _ := cmd.Flags().GetString("client-secret")
req.ClientSecret = &clientSecret
}
if cmd.Flags().Changed("webhook-secret") {
secret, _ := cmd.Flags().GetString("webhook-secret")
req.WebhookSecret = &secret
}
if cmd.Flags().Changed("private-key-uuid") {
uuid, _ := cmd.Flags().GetString("private-key-uuid")
req.PrivateKeyUUID = &uuid
}
if cmd.Flags().Changed("system-wide") {
systemWide, _ := cmd.Flags().GetBool("system-wide")
req.IsSystemWide = &systemWide
}
svc := service.NewGitHubAppService(client)
err = svc.Update(ctx, appUUID, req)
if err != nil {
return fmt.Errorf("failed to update GitHub App: %w", err)
}
fmt.Println("GitHub App updated successfully")
return nil
},
}
updateCmd.Flags().String("name", "", "GitHub App name")
updateCmd.Flags().String("organization", "", "GitHub organization")
updateCmd.Flags().String("api-url", "", "GitHub API URL")
updateCmd.Flags().String("html-url", "", "GitHub HTML URL")
updateCmd.Flags().String("custom-user", "", "Custom user for SSH")
updateCmd.Flags().Int("custom-port", 0, "Custom port for SSH")
updateCmd.Flags().Int("app-id", 0, "GitHub App ID")
updateCmd.Flags().Int("installation-id", 0, "GitHub Installation ID")
updateCmd.Flags().String("client-id", "", "GitHub OAuth Client ID")
updateCmd.Flags().String("client-secret", "", "GitHub OAuth Client Secret")
updateCmd.Flags().String("webhook-secret", "", "GitHub Webhook Secret")
updateCmd.Flags().String("private-key-uuid", "", "UUID of private key")
updateCmd.Flags().Bool("system-wide", false, "Is this app system-wide")
return updateCmd
}
-306
View File
@@ -1,306 +0,0 @@
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var instancesCmd = &cobra.Command{
Use: "instances",
Short: "Coolify instance related commands.",
}
var instanceVersionCmd = &cobra.Command{
Use: "version",
Short: "Get instance version.",
Run: func(cmd *cobra.Command, args []string) {
data, err := Fetch("version")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(data)
},
}
var listInstancesCmd = &cobra.Command{
Use: "list",
Short: "List all Coolify instances.",
Run: func(cmd *cobra.Command, args []string) {
instances := viper.Get("instances").([]interface{})
if PrettyMode {
var prettyJSON bytes.Buffer
instancesBytes, err := json.Marshal(instances)
if err != nil {
fmt.Println(err)
return
}
err = json.Indent(&prettyJSON, instancesBytes, "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(prettyJSON.String())
return
}
if JsonMode {
instancesBytes, err := json.Marshal(instances)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(instancesBytes))
return
}
fmt.Fprintln(w, "#\tName\tFqdn\tToken\tDefault")
for index, entry := range instances {
entryMap, ok := entry.(map[string]interface{})
if !ok {
fmt.Println("Error")
return
}
if ShowSensitive {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n", index+1, entryMap["name"], entryMap["fqdn"], entryMap["token"], map[bool]string{true: "true", false: ""}[entryMap["default"] == true])
} else {
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n", index+1, entryMap["name"], entryMap["fqdn"], SensitiveInformationOverlay, map[bool]string{true: "true", false: ""}[entryMap["default"] == true])
}
}
w.Flush()
fmt.Println("\nNote: Use -s to show sensitive information.")
},
}
var addInstanceCmd = &cobra.Command{
Use: "add",
Example: `add <instanceName> <fqdn> <token>`,
Args: cobra.ExactArgs(3),
Short: "Add a Coolify instance.",
Run: func(cmd *cobra.Command, args []string) {
Name := args[0]
Host := args[1]
Token := args[2]
instances := viper.Get("instances").([]interface{})
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
if Force {
instanceMap["token"] = Token
if SetDefaultInstance {
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
delete(instanceMap, "default")
}
instanceMap["default"] = true
fmt.Printf("%s already exists. Force overwriting. Setting it as default. \n", Name)
} else {
fmt.Printf("%s already exists. Force overwriting. \n", Name)
}
viper.Set("instances", instances)
viper.WriteConfig()
return
}
fmt.Printf("%s already exists. \n", Name)
fmt.Println("\nNote: Use -f to force overwrite.")
return
}
}
instances = append(instances, map[string]interface{}{
"name": Name,
"fqdn": Host,
"token": Token,
})
if SetDefaultInstance {
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
delete(instanceMap, "default")
}
instances[len(instances)-1].(map[string]interface{})["default"] = true
}
viper.Set("instances", instances)
viper.WriteConfig()
listInstancesCmd.Run(cmd, args)
},
}
var removeInstanceCmd = &cobra.Command{
Use: "remove",
Example: `remove <instanceName>`,
Args: cobra.ExactArgs(1),
Short: "Remove a Coolify instance.",
Run: func(cmd *cobra.Command, args []string) {
Name := args[0]
instances := viper.Get("instances").([]interface{})
for i, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
instances = append(instances[:i], instances[i+1:]...)
viper.Set("instances", instances)
viper.WriteConfig()
fmt.Printf("%s removed. \n", Name)
if instanceMap["default"] == true {
fmt.Println("Note: The default instance has been removed.")
if len(instances) > 0 {
instances[0].(map[string]interface{})["default"] = true
viper.Set("instances", instances)
viper.WriteConfig()
fmt.Printf("%s set as default. \n", instances[0].(map[string]interface{})["fqdn"])
}
}
return
}
}
fmt.Printf("%s not found. \n", Name)
},
}
var setCmd = &cobra.Command{
Use: "set",
Args: cobra.ExactArgs(2),
Short: "Set default instance or token.",
Run: func(cmd *cobra.Command, args []string) {
},
}
var setTokenCmd = &cobra.Command{
Use: "token",
Example: `set token <instanceName> "<token>"`,
Args: cobra.ExactArgs(2),
Short: "Set token for the given Coolify instance.",
Run: func(cmd *cobra.Command, args []string) {
Name = args[0]
Token = args[1]
var found interface{}
for _, instance := range viper.Get("instances").([]interface{}) {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
found = instanceMap
break
}
}
if found == nil {
fmt.Printf("%s instance is not found. \n", Name)
return
}
instances := viper.Get("instances").([]interface{})
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
instanceMap["token"] = Token
}
}
viper.Set("instances", instances)
viper.WriteConfig()
listInstancesCmd.Run(cmd, args)
},
}
var setDefaultCmd = &cobra.Command{
Use: "default",
Example: `set default <instanceName>`,
Args: cobra.ExactArgs(1),
Short: "Set the default Coolify instance.",
Run: func(cmd *cobra.Command, args []string) {
Name := args[0]
instances := viper.Get("instances").([]interface{})
var found interface{}
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
found = instanceMap
break
}
}
if found == nil {
fmt.Printf("%s not found. \n", Name)
return
}
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
instanceMap["default"] = true
} else {
delete(instanceMap, "default")
}
}
viper.Set("instances", instances)
viper.WriteConfig()
listInstancesCmd.Run(cmd, args)
},
}
var getInstanceCmd = &cobra.Command{
Use: "get",
Example: `config get <instanceName>`,
Args: cobra.ExactArgs(1),
Short: "Get a Coolify instance.",
Run: func(cmd *cobra.Command, args []string) {
Name := args[0]
instances := viper.Get("instances").([]interface{})
if PrettyMode {
var prettyJSON bytes.Buffer
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
instanceMap["token"] = SensitiveInformationOverlay
}
instancesBytes, err := json.Marshal(instances)
if err != nil {
fmt.Println(err)
return
}
err = json.Indent(&prettyJSON, instancesBytes, "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(prettyJSON.String())
return
}
if JsonMode {
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
instanceMap["token"] = SensitiveInformationOverlay
}
instancesBytes, err := json.Marshal(instances)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(instancesBytes))
return
}
for _, instance := range instances {
instanceMap := instance.(map[string]interface{})
if instanceMap["name"] == Name {
fmt.Fprintln(w, "Name\tHost\tToken")
if ShowSensitive {
fmt.Fprintf(w, "%s\t%s\t%s\n", Name, instanceMap["fqdn"], instanceMap["token"])
} else {
fmt.Fprintf(w, "%s\t%s\t%s\n", Name, instanceMap["fqdn"], SensitiveInformationOverlay)
}
w.Flush()
fmt.Println("\nNote: Use -s to show sensitive information.")
return
}
}
fmt.Printf("%s not found. \n", Name)
},
}
func init() {
addInstanceCmd.Flags().BoolVarP(&SetDefaultInstance, "default", "d", false, "Set default instance")
rootCmd.AddCommand(instancesCmd)
instancesCmd.AddCommand(instanceVersionCmd)
instancesCmd.AddCommand(listInstancesCmd)
instancesCmd.AddCommand(addInstanceCmd)
instancesCmd.AddCommand(removeInstanceCmd)
instancesCmd.AddCommand(setCmd)
instancesCmd.AddCommand(getInstanceCmd)
setCmd.AddCommand(setTokenCmd)
setCmd.AddCommand(setDefaultCmd)
}
-192
View File
@@ -1,192 +0,0 @@
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
"strings"
"github.com/spf13/cobra"
)
type PrivateKeys struct {
PrivateKeys []PrivateKey `json:"private_keys"`
}
type PrivateKey struct {
ID int `json:"id"`
UUID string `json:"uuid"`
Name string `json:"name"`
PublicKey string `json:"public_key"`
PrivateKey string `json:"private_key"`
}
var privateKeysCmd = &cobra.Command{
Use: "private-keys",
Short: "Private key related commands",
}
var listPrivateKeysCmd = &cobra.Command{
Use: "list",
Short: "List all private keys",
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
baseUrl := "security/keys"
data, err := Fetch(baseUrl)
if err != nil {
log.Println(err)
return
}
if PrettyMode {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(prettyJSON.String()))
return
}
if JsonMode {
fmt.Println(data)
return
}
var jsondata []PrivateKey
err = json.Unmarshal([]byte(data), &jsondata)
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintln(w, "Uuid\tName")
for _, resource := range jsondata {
if ShowSensitive {
fmt.Fprintf(w, "%s\t%s\n", resource.UUID, resource.Name)
} else {
fmt.Fprintf(w, "%s\t%s\n", resource.UUID, resource.Name)
}
}
w.Flush()
fmt.Println("\nNote: Use -s to show sensitive information.")
},
}
var onePrivateKeyCmd = &cobra.Command{
Use: "get [uuid]",
Args: cobra.ExactArgs(1),
Short: "Get private key details by uuid",
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
baseUrl := "security/keys/"
uuid := args[0]
var url = baseUrl + uuid
data, err := Fetch(url)
if err != nil {
fmt.Println(err)
return
}
if PrettyMode {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(prettyJSON.String()))
return
}
if JsonMode {
fmt.Println(data)
return
}
var jsondata PrivateKey
err = json.Unmarshal([]byte(data), &jsondata)
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintln(w, "Uuid\tName\tPublicKey\tPrivateKey")
if ShowSensitive {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", jsondata.UUID, jsondata.Name, jsondata.PublicKey, strings.ReplaceAll(jsondata.PrivateKey, "\n", "\\n"))
} else {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", jsondata.UUID, jsondata.Name, SensitiveInformationOverlay, SensitiveInformationOverlay)
}
w.Flush()
fmt.Println("\nNote: Use -s to show sensitive information.")
},
}
var addPrivateKeyCmd = &cobra.Command{
Use: "add",
Example: `add <name> <private_key_or_file>`,
Args: cobra.ExactArgs(2),
Short: "Add a private key",
Run: func(cmd *cobra.Command, args []string) {
version := "4.0.0-beta.383"
CheckDefaultThings(&version)
baseUrl := "security/keys"
name := args[0]
privateKeyInput := args[1]
var privateKey string
// Check if input is a file path
if _, err := os.Stat(privateKeyInput); err == nil {
keyBytes, err := os.ReadFile(privateKeyInput)
if err != nil {
fmt.Printf("Error reading private key file: %v\n", err)
return
}
privateKey = string(keyBytes)
} else {
privateKey = privateKeyInput
}
data := map[string]string{
"name": name,
"private_key": privateKey,
}
jsonData, err := json.Marshal(data)
if err != nil {
fmt.Printf("Error creating request: %v\n", err)
return
}
_, err = Post(baseUrl, bytes.NewBuffer(jsonData))
if err != nil {
fmt.Printf("Error adding private key: %v\n", err)
return
}
fmt.Printf("Private key '%s' added successfully\n", name)
},
}
var removePrivateKeyCmd = &cobra.Command{
Use: "remove [uuid]",
Args: cobra.ExactArgs(1),
Short: "Remove a private key",
Run: func(cmd *cobra.Command, args []string) {
version := "4.0.0-beta.383"
CheckDefaultThings(&version)
baseUrl := "security/keys/"
uuid := args[0]
_, err := Delete(baseUrl + uuid)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Private key removed successfully")
},
}
func init() {
rootCmd.AddCommand(privateKeysCmd)
privateKeysCmd.AddCommand(listPrivateKeysCmd)
privateKeysCmd.AddCommand(onePrivateKeyCmd)
privateKeysCmd.AddCommand(addPrivateKeyCmd)
privateKeysCmd.AddCommand(removePrivateKeyCmd)
}
+58
View File
@@ -0,0 +1,58 @@
package privatekeys
import (
"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"
)
// NewCreateCommand creates the create command
func NewCreateCommand() *cobra.Command {
return &cobra.Command{
Use: "add <key_name> <private_key_or_file>",
Example: `add mykey ~/.ssh/id_rsa`,
Args: cli.ExactArgs(2, "<key_name> <private_key_or_file>"),
Short: "Add a private key",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
name := args[0]
privateKeyInput := args[1]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
var privateKey string
// Check if input is a file path
if _, err := os.Stat(privateKeyInput); err == nil {
keyBytes, err := os.ReadFile(privateKeyInput)
if err != nil {
return fmt.Errorf("error reading private key file: %w", err)
}
privateKey = string(keyBytes)
} else {
privateKey = privateKeyInput
}
keySvc := service.NewPrivateKeyService(client)
req := models.PrivateKeyCreateRequest{
Name: name,
PrivateKey: privateKey,
}
key, err := keySvc.Create(ctx, req)
if err != nil {
return fmt.Errorf("failed to add private key: %w", err)
}
fmt.Printf("Private key '%s' added successfully (UUID: %s)\n", key.Name, key.UUID)
return nil
},
}
}
+37
View File
@@ -0,0 +1,37 @@
package privatekeys
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewDeleteCommand creates the delete command
func NewDeleteCommand() *cobra.Command {
return &cobra.Command{
Use: "remove <uuid>",
Args: cli.ExactArgs(1, "<uuid>"),
Short: "Remove a private key",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
keySvc := service.NewPrivateKeyService(client)
err = keySvc.Delete(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to remove private key: %w", err)
}
fmt.Println("Private key removed successfully")
return nil
},
}
}
+53
View File
@@ -0,0 +1,53 @@
package privatekeys
import (
"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 creates the list command
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all private keys",
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)
}
keySvc := service.NewPrivateKeyService(client)
keys, err := keySvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list private keys: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
if err := formatter.Format(keys); err != nil {
return err
}
if !showSensitive && format == output.FormatTable {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
}
+22
View File
@@ -0,0 +1,22 @@
package privatekeys
import (
"github.com/spf13/cobra"
)
// NewDomainsCommand creates the domains parent command
func NewPrivateKeysCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "private-key",
Aliases: []string{"private-keys", "key", "keys"},
Short: "Private key related commands",
Long: `Manage SSH private keys for server authentication - list, add, and remove keys.`,
}
// Add subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewCreateCommand())
cmd.AddCommand(NewDeleteCommand())
return cmd
}
+77
View File
@@ -0,0 +1,77 @@
package project
import (
"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"
)
// EnvironmentRow represents an environment for display
type EnvironmentRow struct {
UUID string `json:"environment_uuid"`
EnvironmentName string `json:"environment_name"`
}
// NewGetCommand returns the get project command
func NewGetCommand() *cobra.Command {
return &cobra.Command{
Use: "get <uuid>",
Short: "Get a project by uuid",
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
projectSvc := service.NewProjectService(client)
project, err := projectSvc.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get project: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// For JSON/pretty formats, return the full project structure
if format != output.FormatTable {
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(project)
}
// For table format, expand environments into separate rows
var rows []EnvironmentRow
// If the project has environments, expand them
if len(project.Environments) > 0 {
for _, env := range project.Environments {
rows = append(rows, EnvironmentRow{
UUID: env.UUID,
EnvironmentName: env.Name,
})
}
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(rows)
},
}
}
+77
View File
@@ -0,0 +1,77 @@
package project
import (
"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"
)
// ListRow represents a project for list display (without environments)
type ListRow struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Description string `json:"description"`
}
// NewListCommand returns the list projects command
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all projects",
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
projectSvc := service.NewProjectService(client)
projects, err := projectSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list projects: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
// For JSON/pretty formats, return the full project structure
if format != output.FormatTable {
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(projects)
}
// For table format, convert to simplified rows without environments
var rows []ListRow
for _, p := range projects {
desc := ""
if p.Description != nil {
desc = *p.Description
}
rows = append(rows, ListRow{
UUID: p.UUID,
Name: p.Name,
Description: desc,
})
}
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(rows)
},
}
}
+19
View File
@@ -0,0 +1,19 @@
package project
import "github.com/spf13/cobra"
// NewProjectCommand creates the project parent command with all subcommands
func NewProjectCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "project",
Aliases: []string{"projects"},
Short: "Project related commands",
Long: `Manage Coolify projects - list all projects or get details about a specific project.`,
}
// Add all project subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
return cmd
}
-211
View File
@@ -1,211 +0,0 @@
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"log"
"strings"
"github.com/spf13/cobra"
)
type Application struct {
ID int `json:"id"`
Uuid string `json:"uuid"`
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status"`
}
type Environment struct {
ID int `json:"id"`
Uuid string `json:"uuid"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Description *string `json:"description"`
Applications []Application `json:"applications"`
}
type Project struct {
Uuid string `json:"uuid"`
Name string `json:"name"`
Environments []Environment `json:"environments"`
}
var projectsCmd = &cobra.Command{
Use: "projects",
Short: "Project related commands",
}
var listProjectsCmd = &cobra.Command{
Use: "list",
Short: "List all projects",
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
baseUrl := "projects"
data, err := Fetch(baseUrl)
if err != nil {
log.Println(err)
return
}
if PrettyMode {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(prettyJSON.String()))
return
}
if JsonMode {
fmt.Println(data)
return
}
var jsondata []Project
err = json.Unmarshal([]byte(data), &jsondata)
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintln(w, "Uuid\tName")
for _, resource := range jsondata {
fmt.Fprintf(w, "%s\t%s\n", resource.Uuid, resource.Name)
}
w.Flush()
},
}
var oneProjectCmd = &cobra.Command{
Use: "get [uuid]",
Short: "Get a project by uuid",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
uuid := args[0]
environment, _ := cmd.Flags().GetString("environment")
if environment != "" {
url := "projects/" + uuid + "/" + environment
data, err := Fetch(url)
if err != nil {
log.Println(err)
return
}
if PrettyMode {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(prettyJSON.String()))
return
}
if JsonMode {
fmt.Println(data)
return
}
var jsondata Environment
err = json.Unmarshal([]byte(data), &jsondata)
if err != nil {
log.Println(err)
return
}
fmt.Fprintln(w, "Uuid\tName\tStatus")
for _, resource := range jsondata.Applications {
fmt.Fprintf(w, "%s\t%s\t%s\n", resource.Uuid, resource.Name, resource.Status)
}
w.Flush()
return
}
data, err := Fetch("projects/" + uuid)
if err != nil {
log.Println(err)
return
}
if PrettyMode {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(prettyJSON.String()))
return
}
if JsonMode {
fmt.Println(data)
return
}
var jsondata Project
err = json.Unmarshal([]byte(data), &jsondata)
if err != nil {
log.Println(err)
return
}
fmt.Fprintln(w, "Uuid\tName\tEnvironments")
envNames := make([]string, len(jsondata.Environments))
for i, env := range jsondata.Environments {
envNames[i] = env.Name + " (" + env.Uuid + ")"
}
fmt.Fprintf(w, "%s\t%s\t%s\n", jsondata.Uuid, jsondata.Name, strings.Join(envNames, ", "))
w.Flush()
},
}
var addProjectCmd = &cobra.Command{
Use: "add [name]",
Short: "Add a project",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
baseUrl := "projects"
name := args[0]
data := map[string]string{
"name": name,
}
jsonData, err := json.Marshal(data)
if err != nil {
fmt.Println(err)
return
}
response, err := Post(baseUrl, bytes.NewBuffer(jsonData))
if err != nil {
fmt.Println(err)
return
}
msg := map[string]string{}
json.Unmarshal([]byte(response), &msg)
fmt.Println("Project added successfully with uuid " + msg["uuid"])
},
}
var removeProjectCmd = &cobra.Command{
Use: "remove [uuid]",
Short: "Remove a project",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
baseUrl := "projects/"
uuid := args[0]
response, err := Delete(baseUrl + uuid)
if err != nil {
fmt.Println(err)
return
}
msg := map[string]string{}
json.Unmarshal([]byte(response), &msg)
fmt.Println(msg["message"])
},
}
func init() {
rootCmd.AddCommand(projectsCmd)
projectsCmd.AddCommand(listProjectsCmd)
oneProjectCmd.Flags().StringP("environment", "e", "", "Environment")
projectsCmd.AddCommand(oneProjectCmd)
projectsCmd.AddCommand(addProjectCmd)
projectsCmd.AddCommand(removeProjectCmd)
}
-58
View File
@@ -1,58 +0,0 @@
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"log"
"github.com/spf13/cobra"
)
var resourcesCmd = &cobra.Command{
Use: "resources",
Short: "Resource related commands",
}
var listResourcesCmd = &cobra.Command{
Use: "list",
Short: "List all resources",
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
data, err := Fetch("resources")
if err != nil {
log.Println(err)
return
}
if PrettyMode {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(prettyJSON.String()))
return
}
if JsonMode {
fmt.Println(data)
return
}
var jsondata []Resource
err = json.Unmarshal([]byte(data), &jsondata)
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintln(w, "Uuid\tName\tType\tStatus")
for _, resource := range jsondata {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t\n", resource.Uuid, resource.Name, resource.Type, resource.Status)
}
w.Flush()
},
}
func init() {
rootCmd.AddCommand(resourcesCmd)
resourcesCmd.AddCommand(listResourcesCmd)
}
+45
View File
@@ -0,0 +1,45 @@
package resources
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 list projects command
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all resources",
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)
}
resourceSvc := service.NewResourceService(client)
resources, err := resourceSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list resources: %w", err)
}
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
return formatter.Format(resources)
},
}
}
+20
View File
@@ -0,0 +1,20 @@
package resources
import (
"github.com/spf13/cobra"
)
// NewResourceCommand creates the resource parent command with all subcommands
func NewResourceCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "resource",
Aliases: []string{"resources"},
Short: "Resource related commands",
Long: `List all resources (applications, services, databases) in Coolify.`,
}
// Add all resource subcommands
cmd.AddCommand(NewListCommand())
return cmd
}
+102 -306
View File
@@ -1,352 +1,148 @@
package cmd
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"sort"
"text/tabwriter"
"time"
"github.com/adrg/xdg"
compareVersion "github.com/hashicorp/go-version"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/coollabsio/coolify-cli/cmd/application"
"github.com/coollabsio/coolify-cli/cmd/completion"
configcmd "github.com/coollabsio/coolify-cli/cmd/config"
"github.com/coollabsio/coolify-cli/cmd/context"
"github.com/coollabsio/coolify-cli/cmd/database"
"github.com/coollabsio/coolify-cli/cmd/deployment"
"github.com/coollabsio/coolify-cli/cmd/github"
"github.com/coollabsio/coolify-cli/cmd/privatekeys"
"github.com/coollabsio/coolify-cli/cmd/project"
"github.com/coollabsio/coolify-cli/cmd/resources"
"github.com/coollabsio/coolify-cli/cmd/server"
"github.com/coollabsio/coolify-cli/cmd/service"
"github.com/coollabsio/coolify-cli/cmd/teams"
"github.com/coollabsio/coolify-cli/cmd/update"
cliversion "github.com/coollabsio/coolify-cli/cmd/version"
"github.com/coollabsio/coolify-cli/internal/config"
"github.com/coollabsio/coolify-cli/internal/version"
)
var CliVersion = "0.0.1"
var LastUpdateCheckTime time.Time
var CheckInverval = 10 * time.Minute
var ConfigDir = xdg.ConfigHome
var Version string
var Name string
var Fqdn string
var Token string
var Instance http.Client
var SensitiveInformationOverlay = "********"
// Flags
var Debug bool
var ShowSensitive bool
var Force bool
var Format string
var JsonMode bool
var PrettyMode bool
var SetDefaultInstance bool
var w = tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug)
type Tag struct {
Ref string `json:"ref"`
}
// Legacy global variables - kept for backward compatibility during migration
// TODO: Remove these once all commands are refactored
var (
Version string
Name string
Fqdn string
Token string
ContextName string
Debug bool
ShowSensitive bool
Format string
JSONMode bool
PrettyMode bool
SetDefaultInstance bool
)
var rootCmd = &cobra.Command{
Use: "coolify",
Short: "Coolify CLI",
Long: `A CLI tool to interact with Coolify API.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
Use: "coolify",
Short: "Coolify CLI",
Long: `A CLI tool to interact with Coolify API.`,
SilenceUsage: true, // Don't show usage on errors
SilenceErrors: false, // Still print errors
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
return nil
},
}
func CheckFormat(format string) {
if format == "json" {
JsonMode = true
return
}
if format == "pretty" {
PrettyMode = true
return
}
if format == "table" {
return
}
fmt.Println("Invalid format", format)
os.Exit(0)
}
func CheckDefaultThings(version *string) {
FetchVersion()
CheckFormat(Format)
if version == nil {
CheckMinimumVersion(Version)
} else {
CheckMinimumVersion(*version)
}
}
func CheckMinimumVersion(version string) {
requiredVersion, err := compareVersion.NewVersion(version)
if err != nil {
log.Println(err)
os.Exit(0)
}
currentVersion, err := compareVersion.NewVersion(Version)
if err != nil {
log.Println(err)
os.Exit(0)
}
if currentVersion.LessThan(requiredVersion) {
log.Printf("Minimum required Coolify API version is: %s\n", version)
log.Print("Please upgrade your Coolify instance for this command.\n")
os.Exit(0)
}
}
func FetchVersion() (string, error) {
data, err := Fetch("version")
if err != nil {
log.Println(err)
os.Exit(0)
return "", err
}
Version = data
return data, nil
}
func Fetch(url string) (string, error) {
url = Fqdn + "/api/v1/" + url
if Debug {
log.Println("Fetching data from", url)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
req.Header.Add("Authorization", "Bearer "+Token)
resp, err := Instance.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("%d - Failed to fetch data from %s. Error: %s", resp.StatusCode, url, string(body))
}
return string(body), nil
}
func Post(url string, input io.Reader) (string, error) {
url = Fqdn + "/api/v1/" + url
if Debug {
log.Println("Posting data to", url)
}
req, err := http.NewRequest("POST", url, input)
if err != nil {
return "", err
}
req.Header.Add("Authorization", "Bearer "+Token)
req.Header.Add("Content-Type", "application/json")
resp, err := Instance.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode >= 400 {
message := string(body)
if message == "" {
message = "Unknown error"
} else {
msg := map[string]string{}
json.Unmarshal(body, &msg)
message = msg["message"]
}
return "", fmt.Errorf("%s (rc: %d)", message, resp.StatusCode)
}
return string(body), nil
}
func Delete(url string) (string, error) {
url = Fqdn + "/api/v1/" + url
if Debug {
log.Println("Deleting data from", url)
}
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return "", err
}
req.Header.Add("Authorization", "Bearer "+Token)
resp, err := Instance.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != 200 {
message := string(body)
if message == "" {
message = "Unknown error"
} else {
msg := map[string]string{}
json.Unmarshal(body, &msg)
message = msg["message"]
}
return "", fmt.Errorf("%s (rc: %d)", message, resp.StatusCode)
}
return string(body), nil
}
func CheckLatestVersionOfCli() (string, error) {
getLastUpdateCheckTime()
if LastUpdateCheckTime.Add(CheckInverval).After(time.Now()) {
if Debug {
log.Println("Skipping update check. Last check was less than 10 minutes ago.")
}
return CliVersion, nil
}
setLastUpdateCheckTime()
url := "https://api.github.com/repos/coollabsio/coolify-cli/git/refs/tags"
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("%d - Failed to fetch data from %s. Error: %s", resp.StatusCode, url, string(body))
}
var tags []Tag
if err := json.Unmarshal(body, &tags); err != nil {
return "", err
}
versionsRaw := make([]string, 0, len(tags))
for _, tag := range tags {
versionStr := tag.Ref[10:]
versionsRaw = append(versionsRaw, versionStr)
}
versions := make([]*compareVersion.Version, len(versionsRaw))
for i, raw := range versionsRaw {
v, err := compareVersion.NewVersion(raw)
if err != nil {
return "", err
}
versions[i] = v
}
sort.Sort(compareVersion.Collection(versions))
latestVersion := versions[len(versions)-1].String()
if latestVersion != CliVersion {
fmt.Printf("There is a new version of Coolify CLI available.\nPlease update with 'coolify --update'.\n\n")
}
return latestVersion, nil
}
// Execute runs the root command
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(0)
os.Exit(1)
}
}
func init() {
rootCmd = &cobra.Command{
Use: "coolify",
Short: "Coolify CLI",
Long: fmt.Sprintf("A CLI tool to interact with Coolify API.\nVersion: %s", version.GetVersion()),
SilenceUsage: true, // Don't show usage on errors
SilenceErrors: false, // Still print errors
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
return nil
},
}
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVarP(&Token, "token", "", "", "Token for authentication (https://app.coolify.io/security/api-tokens)")
rootCmd.PersistentFlags().StringVarP(&Fqdn, "host", "", "", "Coolify instance hostname")
rootCmd.PersistentFlags().StringVarP(&Token, "token", "", "", "Token for authentication (override context token)")
rootCmd.PersistentFlags().StringVarP(&ContextName, "context", "", "", "Use specific context by name")
rootCmd.PersistentFlags().StringVarP(&Format, "format", "", "table", "Format output (table|json|pretty)")
rootCmd.PersistentFlags().BoolVarP(&ShowSensitive, "show-sensitive", "s", false, "Show sensitive information")
rootCmd.PersistentFlags().BoolVarP(&Force, "force", "f", false, "Force")
rootCmd.PersistentFlags().BoolVarP(&Debug, "debug", "", false, "Debug mode")
}
func setLastUpdateCheckTime() {
timeNow := time.Now()
viper.Set("lastupdatechecktime", timeNow)
viper.WriteConfig()
LastUpdateCheckTime = timeNow
}
func getLastUpdateCheckTime() {
lastUpdateCheckTimeString := viper.Get("lastupdatechecktime").(string)
lastUpdateCheckTime, err := time.Parse(time.RFC3339, lastUpdateCheckTimeString)
if err != nil {
log.Fatalf("Error parsing time: %v", err)
}
LastUpdateCheckTime = lastUpdateCheckTime
// Register all subcommands
rootCmd.AddCommand(application.NewAppCommand())
rootCmd.AddCommand(completion.NewCompletionsCommand())
rootCmd.AddCommand(configcmd.NewConfigCommand())
rootCmd.AddCommand(context.NewContextCommand())
rootCmd.AddCommand(database.NewDatabaseCommand())
rootCmd.AddCommand(deployment.NewDeploymentCommand())
rootCmd.AddCommand(github.NewGitHubCommand())
rootCmd.AddCommand(privatekeys.NewPrivateKeysCommand())
rootCmd.AddCommand(project.NewProjectCommand())
rootCmd.AddCommand(resources.NewResourceCommand())
rootCmd.AddCommand(server.NewServerCommand())
rootCmd.AddCommand(service.NewServiceCommand())
rootCmd.AddCommand(teams.NewTeamsCommand())
rootCmd.AddCommand(update.NewUpdateCommand())
rootCmd.AddCommand(cliversion.NewVersionCommand())
rootCmd.AddCommand(NewDocsCommand())
}
func initConfig() {
viper.SetConfigName("config")
viper.SetConfigType("json")
viper.AddConfigPath(ConfigDir + "/coolify")
if _, err := os.Stat(ConfigDir + "/coolify"); os.IsNotExist(err) {
os.MkdirAll(ConfigDir+"/coolify", 0755)
viper.AddConfigPath(config.Path()[:len(config.Path())-len("/config.json")])
// Ensure config directory exists
configDir := config.Path()[:len(config.Path())-len("/config.json")]
if _, err := os.Stat(configDir); os.IsNotExist(err) {
if err := os.MkdirAll(configDir, 0750); err != nil {
log.Printf("Failed to create config directory: %v\n", err)
}
}
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Println("Config file not found. Creating a new one at", ConfigDir+"/coolify/config.json")
viper.Set("lastUpdateCheckTime", time.Now())
viper.Set("instances", []interface{}{map[string]interface{}{
"name": "cloud",
"default": true,
"fqdn": "https://app.coolify.io",
"token": "",
},
})
viper.Set("instances", append(viper.Get("instances").([]interface{}), map[string]interface{}{
"name": "localhost",
"fqdn": "http://localhost:8000",
"token": "",
}))
viper.SafeWriteConfig()
return
// Config file not found; ignore error if desired
var notFoundErr viper.ConfigFileNotFoundError
if errors.As(err, &notFoundErr) {
log.Println("Config file not found. Creating a new one at", config.Path())
if err := config.CreateDefault(); err != nil {
log.Printf("Failed to create default config: %v\n", err)
return
}
// Reload config after creating default
if err := viper.ReadInConfig(); err != nil {
log.Printf("Failed to read newly created config: %v\n", err)
return
}
} else {
fmt.Println("Error reading config file, ", err)
fmt.Println("Error reading config file:", err)
return
// Config file was found but another error was produced
}
}
if Debug {
log.Println("Using config file:", viper.ConfigFileUsed())
}
instancesMap := viper.Get("instances").([]interface{})
for _, instance := range instancesMap {
instanceMap := instance.(map[string]interface{})
if instanceMap["default"] == true {
if Fqdn == "" {
Fqdn = instanceMap["fqdn"].(string)
}
if Token == "" {
Token = instanceMap["token"].(string)
}
}
}
data, err := CheckLatestVersionOfCli()
if err != nil {
log.Println(err)
}
if data != CliVersion {
log.Printf("New version of Coolify CLI is available: %s\n", data)
}
// Note: We don't pre-populate Fqdn/Token here anymore
// They are loaded on-demand by getAPIClient() based on --instance or default instance
// This allows --instance flag to work correctly
// Check for updates (errors are handled silently inside the function)
_, _ = version.CheckLatestVersionOfCli(Debug)
}
+68
View File
@@ -0,0 +1,68 @@
package server
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"
)
// NewAddCommand creates the add command
func NewAddCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "add <server_name> <ip_address> <private_key_uuid>",
Args: cli.ExactArgs(3, "<server_name> <ip_address> <private_key_uuid>"),
Short: "Add a server",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Get API client
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
// Parse arguments and flags
name := args[0]
ip := args[1]
privateKeyUUID := args[2]
port, _ := cmd.Flags().GetInt("port")
user, _ := cmd.Flags().GetString("user")
validate, _ := cmd.Flags().GetBool("validate")
// Create request
req := models.ServerCreateRequest{
Name: name,
IP: ip,
Port: port,
User: user,
PrivateKeyUUID: privateKeyUUID,
InstantValidate: validate,
}
// Use service layer
serverSvc := service.NewServerService(client)
response, err := serverSvc.Create(ctx, req)
if err != nil {
return fmt.Errorf("failed to create server: %w", err)
}
if validate {
fmt.Printf("Server added successfully with uuid %s\n", response.UUID)
} else {
fmt.Printf("Server added successfully with uuid %s. Server is not validated. Use 'servers validate %s' to validate the server.\n", response.UUID, response.UUID)
}
return nil
},
}
cmd.Flags().IntP("port", "p", 22, "Port")
cmd.Flags().StringP("user", "u", "root", "User")
cmd.Flags().Bool("validate", false, "Validate the server")
return cmd
}
+63
View File
@@ -0,0 +1,63 @@
package server
import (
"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"
)
// NewGetCommand creates the get command
func NewGetDomainsCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "domains <uuid>",
Aliases: []string{"domain"},
Args: cli.ExactArgs(1, "<uuid>"),
Short: "Get server domains by uuid",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := 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
serverSvc := service.NewServerService(client)
uuid := args[0]
// Get format flags
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
domains, err := serverSvc.GetDomains(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get server domains: %w", err)
}
// Use output formatter
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
if err := formatter.Format(domains); err != nil {
return err
}
if !showSensitive && format == output.FormatTable {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
return cmd
}
+75
View File
@@ -0,0 +1,75 @@
package server
import (
"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"
)
// NewGetCommand creates the get command
func NewGetCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "get <uuid>",
Args: cli.ExactArgs(1, "<uuid>"),
Short: "Get server details by uuid",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := 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
serverSvc := service.NewServerService(client)
uuid := args[0]
// Get format flags
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
withResources, _ := cmd.Flags().GetBool("resources")
var data interface{}
if withResources {
resources, err := serverSvc.GetResources(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get server resources: %w", err)
}
data = resources.Resources
} else {
server, err := serverSvc.Get(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to get server: %w", err)
}
data = server
}
// Use output formatter
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
if err := formatter.Format(data); err != nil {
return err
}
if !showSensitive && format == output.FormatTable && !withResources {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
cmd.Flags().Bool("resources", false, "With resources")
return cmd
}
+56
View File
@@ -0,0 +1,56 @@
package server
import (
"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 creates the list command
func NewListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all servers",
RunE: func(cmd *cobra.Command, _ []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
serverSvc := service.NewServerService(client)
servers, err := serverSvc.List(ctx)
if err != nil {
return fmt.Errorf("failed to list servers: %w", err)
}
// Use output formatter
format, _ := cmd.Flags().GetString("format")
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
formatter, err := output.NewFormatter(format, output.Options{
ShowSensitive: showSensitive,
})
if err != nil {
return err
}
if err := formatter.Format(servers); err != nil {
return err
}
if !showSensitive && format == output.FormatTable {
fmt.Println("\nNote: Use -s to show sensitive information.")
}
return nil
},
}
}
+39
View File
@@ -0,0 +1,39 @@
package server
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewRemoveCommand creates the remove command
func NewRemoveCommand() *cobra.Command {
return &cobra.Command{
Use: "remove <uuid>",
Args: cli.ExactArgs(1, "<uuid>"),
Short: "Remove a server",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := 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
serverSvc := service.NewServerService(client)
uuid := args[0]
if err := serverSvc.Delete(ctx, uuid); err != nil {
return fmt.Errorf("failed to delete server: %w", err)
}
fmt.Printf("Server %s deleted successfully\n", uuid)
return nil
},
}
}
+25
View File
@@ -0,0 +1,25 @@
package server
import (
"github.com/spf13/cobra"
)
// NewServerCommand creates the server parent command
func NewServerCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "server",
Aliases: []string{"servers"},
Short: "Server related commands",
Long: `Manage Coolify servers - list, get details, add new servers, validate connections, and remove servers.`,
}
// Add subcommands
cmd.AddCommand(NewListCommand())
cmd.AddCommand(NewGetCommand())
cmd.AddCommand(NewGetDomainsCommand())
cmd.AddCommand(NewAddCommand())
cmd.AddCommand(NewRemoveCommand())
cmd.AddCommand(NewValidateCommand())
return cmd
}
+45
View File
@@ -0,0 +1,45 @@
package server
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewValidateCommand creates the validate command
func NewValidateCommand() *cobra.Command {
return &cobra.Command{
Use: "validate <uuid>",
Args: cli.ExactArgs(1, "<uuid>"),
Short: "Validate a server",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := 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
serverSvc := service.NewServerService(client)
uuid := args[0]
response, err := serverSvc.Validate(ctx, uuid)
if err != nil {
return fmt.Errorf("failed to validate server: %w", err)
}
if response.Message != "" {
fmt.Println(response.Message)
} else {
fmt.Printf("Server %s validated successfully\n", uuid)
}
return nil
},
}
}
-243
View File
@@ -1,243 +0,0 @@
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"log"
"github.com/spf13/cobra"
)
var WithResources bool
type Resource struct {
ID int `json:"id"`
Uuid string `json:"uuid"`
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
}
type Resources struct {
Resources []Resource `json:"resources"`
}
type Server struct {
ID int `json:"id"`
UUID string `json:"uuid"`
Name string `json:"name"`
IP string `json:"ip"`
User string `json:"user"`
Port int `json:"port"`
Settings struct {
Reachable bool `json:"is_reachable"`
Usable bool `json:"is_usable"`
} `json:"settings"`
}
var serversCmd = &cobra.Command{
Use: "servers",
Short: "Server related commands",
}
var listServersCmd = &cobra.Command{
Use: "list",
Short: "List all servers",
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
baseUrl := "servers"
data, err := Fetch(baseUrl)
if err != nil {
log.Println(err)
return
}
if PrettyMode {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(prettyJSON.String()))
return
}
if JsonMode {
fmt.Println(data)
return
}
var jsondata []Server
err = json.Unmarshal([]byte(data), &jsondata)
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintln(w, "Uuid\tName\tIP Address\tUser\tPort\tReachable\tUsable")
for _, resource := range jsondata {
if ShowSensitive {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%t\t%t\n", resource.UUID, resource.Name, resource.IP, resource.User, resource.Port, resource.Settings.Reachable, resource.Settings.Usable)
} else {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%t\t%t\n", resource.UUID, resource.Name, SensitiveInformationOverlay, SensitiveInformationOverlay, SensitiveInformationOverlay, resource.Settings.Reachable, resource.Settings.Usable)
}
}
w.Flush()
fmt.Println("\nNote: Use -s to show sensitive information.")
},
}
var oneServerCmd = &cobra.Command{
Use: "get [uuid]",
Args: cobra.ExactArgs(1),
Short: "Get server details by uuid",
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
baseUrl := "servers/"
uuid := args[0]
var url = baseUrl + uuid
if WithResources {
url = baseUrl + uuid + "?resources=true"
}
data, err := Fetch(url)
if err != nil {
fmt.Println(err)
return
}
if PrettyMode {
var prettyJSON bytes.Buffer
err := json.Indent(&prettyJSON, []byte(data), "", "\t")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(prettyJSON.String()))
return
}
if JsonMode {
fmt.Println(data)
return
}
if WithResources {
var jsondata Resources
err = json.Unmarshal([]byte(data), &jsondata)
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintln(w, "Uuid\tName\tType\tStatus")
for _, resource := range jsondata.Resources {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t\n", resource.Uuid, resource.Name, resource.Type, resource.Status)
}
w.Flush()
} else {
var jsondata Server
err = json.Unmarshal([]byte(data), &jsondata)
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintln(w, "Uuid\tName\tIP Address\tUser\tPort\tReachable\tUsable")
if ShowSensitive {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\t%t\t%t\n", jsondata.UUID, jsondata.Name, jsondata.IP, jsondata.User, jsondata.Port, jsondata.Settings.Reachable, jsondata.Settings.Usable)
} else {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%t\t%t\n", jsondata.UUID, jsondata.Name, SensitiveInformationOverlay, SensitiveInformationOverlay, SensitiveInformationOverlay, jsondata.Settings.Reachable, jsondata.Settings.Usable)
}
w.Flush()
fmt.Println("\nNote: Use -s to show sensitive information.")
}
},
}
var removeServerCmd = &cobra.Command{
Use: "remove [uuid]",
Short: "Remove a server",
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
baseUrl := "servers/"
uuid := args[0]
response, err := Delete(baseUrl + uuid)
if err != nil {
fmt.Println(err)
return
}
msg := map[string]string{}
json.Unmarshal([]byte(response), &msg)
fmt.Println(msg["message"])
},
}
var addServerCmd = &cobra.Command{
Use: "add [name] [ip] [private_key_uuid]",
Short: "Add a server",
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
baseUrl := "servers"
name := args[0]
ip := args[1]
privateKeyUuid := args[2]
port, _ := cmd.Flags().GetInt("port")
user, _ := cmd.Flags().GetString("user")
validate, _ := cmd.Flags().GetBool("validate")
jsonData, err := json.Marshal(map[string]interface{}{
"name": name,
"ip": ip,
"port": port,
"user": user,
"private_key_uuid": privateKeyUuid,
"instant_validate": validate,
})
if err != nil {
fmt.Println(err)
return
}
response, err := Post(baseUrl, bytes.NewBuffer(jsonData))
if err != nil {
fmt.Println(err)
return
}
msg := map[string]string{}
json.Unmarshal([]byte(response), &msg)
if validate {
fmt.Println("Server added successfully with uuid " + msg["uuid"])
} else {
fmt.Println("Server added successfully with uuid " + msg["uuid"] + ". Server is not validated. Use 'servers validate " + msg["uuid"] + "' to validate the server.")
}
},
}
var validateServerCmd = &cobra.Command{
Use: "validate [uuid]",
Short: "Validate a server",
Run: func(cmd *cobra.Command, args []string) {
CheckDefaultThings(nil)
baseUrl := "servers/"
uuid := args[0]
var url = baseUrl + uuid + "/validate"
response, err := Fetch(url)
if err != nil {
fmt.Println(err)
return
}
msg := map[string]string{}
json.Unmarshal([]byte(response), &msg)
fmt.Println(msg["message"])
},
}
func init() {
oneServerCmd.Flags().BoolVarP(&WithResources, "resources", "", false, "With resources")
rootCmd.AddCommand(serversCmd)
serversCmd.AddCommand(listServersCmd)
serversCmd.AddCommand(oneServerCmd)
addServerCmd.Flags().IntP("port", "p", 22, "Port")
addServerCmd.Flags().StringP("user", "u", "root", "User")
addServerCmd.Flags().BoolP("validate", "", false, "Validate the server")
serversCmd.AddCommand(addServerCmd)
serversCmd.AddCommand(validateServerCmd)
serversCmd.AddCommand(removeServerCmd)
}
+68
View File
@@ -0,0 +1,68 @@
package service
import (
"fmt"
"github.com/spf13/cobra"
"github.com/coollabsio/coolify-cli/internal/cli"
"github.com/coollabsio/coolify-cli/internal/service"
)
// NewDeleteCommand deletes a service
func NewDeleteCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete <uuid>",
Short: "Delete a service",
Long: `Delete a service and optionally clean up its configurations, volumes, and networks.`,
Args: cli.ExactArgs(1, "<uuid>"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
uuid := args[0]
client, err := cli.GetAPIClient(cmd)
if err != nil {
return fmt.Errorf("failed to get API client: %w", err)
}
force, _ := cmd.Flags().GetBool("force")
deleteConfigurations, _ := cmd.Flags().GetBool("delete-configurations")
deleteVolumes, _ := cmd.Flags().GetBool("delete-volumes")
dockerCleanup, _ := cmd.Flags().GetBool("docker-cleanup")
deleteConnectedNetworks, _ := cmd.Flags().GetBool("delete-connected-networks")
// Prompt for confirmation unless --force is used
if !force {
var response string
fmt.Printf("Are you sure you want to delete this service? (yes/no): ")
_, 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.")
return nil
}
}
serviceSvc := service.NewService(client)
err = serviceSvc.Delete(ctx, uuid, deleteConfigurations, deleteVolumes, dockerCleanup, deleteConnectedNetworks)
if err != nil {
return fmt.Errorf("failed to delete service: %w", err)
}
fmt.Println("Service deletion request queued.")
return nil
},
}
cmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
cmd.Flags().Bool("delete-configurations", true, "Delete configurations")
cmd.Flags().Bool("delete-volumes", true, "Delete volumes")
cmd.Flags().Bool("docker-cleanup", true, "Run docker cleanup")
cmd.Flags().Bool("delete-connected-networks", true, "Delete connected networks")
return cmd
}

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