├── .github └── workflows │ ├── build.yml │ ├── docs.yml │ ├── release.yml │ ├── test.yml │ └── update-homebrew.yml ├── .gitignore ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── docgen │ └── main.go └── pops │ ├── app │ ├── conn │ │ ├── cloud │ │ │ ├── cloud.go │ │ │ ├── create.go │ │ │ ├── delete.go │ │ │ ├── list.go │ │ │ ├── open.go │ │ │ └── types.go │ │ ├── connection.go │ │ ├── create.go │ │ ├── db │ │ │ ├── create.go │ │ │ ├── db.go │ │ │ ├── delete.go │ │ │ ├── list.go │ │ │ ├── open.go │ │ │ └── types.go │ │ ├── delete.go │ │ ├── factory │ │ │ ├── create.go │ │ │ ├── doc.go │ │ │ └── open.go │ │ ├── k8s │ │ │ ├── create.go │ │ │ ├── delete.go │ │ │ ├── kubernetes.go │ │ │ ├── list.go │ │ │ ├── open.go │ │ │ └── types.go │ │ ├── list.go │ │ ├── open.go │ │ └── types.go │ ├── root.go │ └── version.go │ └── main.go ├── docs ├── contributing │ ├── CONTRIBUTING.md │ ├── ai │ │ └── ADD_AI_MODEL.md │ └── connection │ │ ├── ADD_SUBTYPE.md │ │ └── ADD_TYPE.md ├── examples │ ├── README.md │ └── conn │ │ └── k8s │ │ ├── README.md │ │ ├── pops_conn_k8s_create_v1.gif │ │ └── pops_conn_k8s_create_v1.mov └── releases │ └── RELEASE_PROCESS.md ├── go.mod ├── go.sum ├── make ├── build.mk ├── format.mk ├── gendocs.mk ├── install.mk ├── lint.mk └── test.mk ├── pkg ├── ai │ ├── openai.go │ └── types.go ├── config │ └── config.go ├── conn │ ├── cloud.go │ ├── cloud_test.go │ ├── db.go │ ├── db_test.go │ ├── factory.go │ ├── kubernetes.go │ ├── kubernetes_test.go │ ├── query.go │ └── types.go └── ui │ ├── conn │ ├── cloud │ │ ├── create.go │ │ ├── create_test.go │ │ ├── doc.go │ │ ├── open.go │ │ └── types.go │ ├── db │ │ ├── create.go │ │ ├── doc.go │ │ ├── open.go │ │ └── types.go │ ├── k8s │ │ ├── create.go │ │ ├── doc.go │ │ ├── open.go │ │ └── types.go │ └── open.go │ ├── doc.go │ ├── shell │ ├── actions.go │ ├── shell.go │ ├── styles.go │ ├── types.go │ └── views.go │ ├── spinner.go │ ├── table.go │ └── types.go └── scripts └── install.sh /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release/* 8 | tags: 9 | - "v*.*.*" 10 | pull_request: 11 | branches: 12 | - main 13 | - release/* 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | goos: [linux, darwin, windows] 25 | goarch: [amd64, arm64] 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Set up Go 32 | uses: actions/setup-go@v5 33 | with: 34 | go-version: 1.22 35 | cache: false 36 | 37 | - name: Install dependencies 38 | run: go mod tidy 39 | 40 | - name: Run tests 41 | run: go test -v ./... 42 | 43 | - name: Build binaries 44 | run: | 45 | echo "Building for GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }}" 46 | GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \ 47 | make build 48 | 49 | - name: Rename Windows binaries with .exe 50 | if: matrix.goos == 'windows' 51 | run: mv dist/pops-windows-${{ matrix.goarch }} dist/pops-windows-${{ matrix.goarch }}.exe 52 | 53 | - name: Validate binary 54 | run: | 55 | file dist/pops-${{ matrix.goos }}-${{ matrix.goarch }}* 56 | 57 | - name: Upload binaries as artifact 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: pops-${{ matrix.goos }}-${{ matrix.goarch }} 61 | path: dist/* 62 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate and Publish CLI Docs 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | generate-and-publish: 14 | runs-on: ubuntu-latest 15 | 16 | env: 17 | TAG_NAME: ${{ github.event.release.tag_name || 'edge' }} 18 | 19 | steps: 20 | - name: Checkout Repository 21 | uses: actions/checkout@v4 22 | with: 23 | path: pops 24 | 25 | - name: Checkout Docs Repository 26 | uses: actions/checkout@v4 27 | with: 28 | repository: prompt-ops/docs 29 | path: docs 30 | 31 | - name: Set up Go 32 | uses: actions/setup-go@v5 33 | with: 34 | go-version: 1.22 35 | cache: false 36 | 37 | - name: Install dependencies 38 | run: go mod tidy 39 | working-directory: pops 40 | 41 | - name: Generate CLI Docs 42 | run: | 43 | mkdir -p release/docs 44 | make generate-cli-docs OUTPUT_PATH=release/docs/ 45 | working-directory: pops 46 | 47 | - name: Copy Generated Docs 48 | run: | 49 | mkdir -p docs/content/en/cli 50 | cp -R pops/release/docs/* docs/content/en/cli/ 51 | 52 | - name: Create Pull Request 53 | uses: peter-evans/create-pull-request@v7 54 | with: 55 | token: ${{ secrets.POPS_GITHUB_OPS_PAT }} 56 | path: docs 57 | committer: pops-ci-bot 58 | author: pops-ci-bot 59 | signoff: true 60 | commit-message: Update CLI documentation for ${{ env.TAG_NAME }} 61 | title: "📄 Update CLI Documentation for ${{ env.TAG_NAME }}" 62 | body: | 63 | This PR updates the CLI documentation to reflect the changes in `${{ env.TAG_NAME }}`. 64 | 65 | ### Changes 66 | - Updated CLI commands and usage examples. 67 | - Added new features introduced in this release. 68 | 69 | ### How to Test 70 | - Verify the generated documentation in the [docs repository](https://github.com/prompt-ops/docs). 71 | base: main 72 | branch: automated-docs-update/patch-${{ github.sha }} 73 | delete-branch: true 74 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | goos: [linux, darwin, windows] 18 | goarch: [amd64, arm64] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: 1.22 28 | cache: false 29 | 30 | - name: Install dependencies 31 | run: go mod tidy 32 | 33 | - name: Run tests 34 | run: go test -v ./... 35 | 36 | - name: Build binaries 37 | env: 38 | VERSION: ${{ github.ref_name }} 39 | run: | 40 | echo "Building for GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} with VERSION=${VERSION}" 41 | GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \ 42 | make build VERSION=${VERSION} 43 | 44 | - name: Rename Windows binaries with .exe 45 | if: matrix.goos == 'windows' 46 | run: mv dist/pops-windows-${{ matrix.goarch }} dist/pops-windows-${{ matrix.goarch }}.exe 47 | 48 | - name: Validate binary 49 | run: | 50 | file dist/pops-${{ matrix.goos }}-${{ matrix.goarch }}* 51 | 52 | - name: Upload binaries as artifact 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: pops-${{ matrix.goos }}-${{ matrix.goarch }} 56 | path: dist/pops-${{ matrix.goos }}-${{ matrix.goarch }}* 57 | 58 | release: 59 | runs-on: ubuntu-latest 60 | needs: build 61 | 62 | steps: 63 | - name: Checkout 64 | uses: actions/checkout@v4 65 | 66 | - name: Download build artifacts 67 | uses: actions/download-artifact@v4 68 | with: 69 | path: dist/ 70 | pattern: pops-* 71 | 72 | - name: List dist folder 73 | run: ls -R dist/ 74 | 75 | - name: Create GitHub Release 76 | id: create_release 77 | uses: softprops/action-gh-release@v2 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | with: 81 | repository: prompt-ops/pops 82 | files: | 83 | dist/pops-darwin-amd64/pops-darwin-amd64 84 | dist/pops-darwin-arm64/pops-darwin-arm64 85 | dist/pops-linux-amd64/pops-linux-amd64 86 | dist/pops-linux-arm64/pops-linux-arm64 87 | dist/pops-windows-amd64/pops-windows-amd64.exe 88 | dist/pops-windows-arm64/pops-windows-arm64.exe 89 | draft: false 90 | prerelease: false 91 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: 1.22 23 | cache: false 24 | 25 | - name: Install dependencies 26 | run: go mod tidy 27 | 28 | - name: Run unit tests 29 | run: make unit-test 30 | -------------------------------------------------------------------------------- /.github/workflows/update-homebrew.yml: -------------------------------------------------------------------------------- 1 | name: Update Homebrew Tap 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | update-formula: 14 | runs-on: ubuntu-latest 15 | 16 | env: 17 | TAG_NAME: ${{ github.event.release.tag_name || 'edge' }} 18 | 19 | steps: 20 | - name: Checkout the pops repository 21 | uses: actions/checkout@v4 22 | with: 23 | ref: ${{ github.ref }} 24 | 25 | - name: Download & compute checksums 26 | id: checksums 27 | run: | 28 | VERSION=${{ env.TAG_NAME }} 29 | 30 | # Download each artifact 31 | curl -L -o pops-darwin-amd64 \ 32 | https://github.com/prompt-ops/pops/releases/download/${VERSION}/pops-darwin-amd64 33 | curl -L -o pops-darwin-arm64 \ 34 | https://github.com/prompt-ops/pops/releases/download/${VERSION}/pops-darwin-arm64 35 | curl -L -o pops-linux-amd64 \ 36 | https://github.com/prompt-ops/pops/releases/download/${VERSION}/pops-linux-amd64 37 | curl -L -o pops-linux-arm64 \ 38 | https://github.com/prompt-ops/pops/releases/download/${VERSION}/pops-linux-arm64 39 | 40 | # Compute checksums 41 | DARWIN_AMD64_SHA=$(sha256sum pops-darwin-amd64 | cut -d ' ' -f1) 42 | DARWIN_ARM64_SHA=$(sha256sum pops-darwin-arm64 | cut -d ' ' -f1) 43 | LINUX_AMD64_SHA=$(sha256sum pops-linux-amd64 | cut -d ' ' -f1) 44 | LINUX_ARM64_SHA=$(sha256sum pops-linux-arm64 | cut -d ' ' -f1) 45 | 46 | echo "darwin_amd64_sha=${DARWIN_AMD64_SHA}" >> $GITHUB_ENV 47 | echo "darwin_arm64_sha=${DARWIN_ARM64_SHA}" >> $GITHUB_ENV 48 | echo "linux_amd64_sha=${LINUX_AMD64_SHA}" >> $GITHUB_ENV 49 | echo "linux_arm64_sha=${LINUX_ARM64_SHA}" >> $GITHUB_ENV 50 | 51 | - name: Checkout Homebrew Tap 52 | uses: actions/checkout@v4 53 | with: 54 | repository: prompt-ops/homebrew-tap 55 | token: ${{ secrets.POPS_GITHUB_OPS_PAT }} 56 | ref: main 57 | path: homebrew-tap 58 | 59 | - name: Update formula 60 | run: | 61 | VERSION=${{ env.TAG_NAME }} 62 | DARWIN_AMD64_SHA=${{ env.darwin_amd64_sha }} 63 | DARWIN_ARM64_SHA=${{ env.darwin_arm64_sha }} 64 | LINUX_AMD64_SHA=${{ env.linux_amd64_sha }} 65 | LINUX_ARM64_SHA=${{ env.linux_arm64_sha }} 66 | 67 | cd homebrew-tap 68 | FORMULA_FILE="Formula/pops.rb" 69 | 70 | # Use sed to replace version, URLs, and SHAs in the Ruby formula 71 | sed -i.bak "s|^ version \".*\"| version \"${VERSION}\"|" $FORMULA_FILE 72 | 73 | # Darwin Intel 74 | sed -i.bak "s|^ url \"https://github.com/prompt-ops/pops/releases/download/.*pops-darwin-amd64\"| url \"https://github.com/prompt-ops/pops/releases/download/${VERSION}/pops-darwin-amd64\"|" $FORMULA_FILE 75 | sed -i.bak "s|^ sha256 \".*\"| sha256 \"${DARWIN_AMD64_SHA}\"|" $FORMULA_FILE 76 | 77 | # Darwin ARM 78 | sed -i.bak "s|^ url \"https://github.com/prompt-ops/pops/releases/download/.*pops-darwin-arm64\"| url \"https://github.com/prompt-ops/pops/releases/download/${VERSION}/pops-darwin-arm64\"|" $FORMULA_FILE 79 | sed -i.bak "s|^ sha256 \".*\"| sha256 \"${DARWIN_ARM64_SHA}\"|" $FORMULA_FILE 80 | 81 | # Linux Intel 82 | sed -i.bak "s|^ url \"https://github.com/prompt-ops/pops/releases/download/.*pops-linux-amd64\"| url \"https://github.com/prompt-ops/pops/releases/download/${VERSION}/pops-linux-amd64\"|" $FORMULA_FILE 83 | sed -i.bak "s|^ sha256 \".*\"| sha256 \"${LINUX_AMD64_SHA}\"|" $FORMULA_FILE 84 | 85 | # Linux ARM 86 | sed -i.bak "s|^ url \"https://github.com/prompt-ops/pops/releases/download/.*pops-linux-arm64\"| url \"https://github.com/prompt-ops/pops/releases/download/${VERSION}/pops-linux-arm64\"|" $FORMULA_FILE 87 | sed -i.bak "s|^ sha256 \".*\"| sha256 \"${LINUX_ARM64_SHA}\"|" $FORMULA_FILE 88 | 89 | # Remove backup files 90 | rm -f $FORMULA_FILE.bak 91 | 92 | - name: Create Pull Request 93 | uses: peter-evans/create-pull-request@v7 94 | with: 95 | token: ${{ secrets.POPS_GITHUB_OPS_PAT }} 96 | path: "homebrew-tap" 97 | committer: pops-ci-bot 98 | author: pops-ci-bot 99 | signoff: true 100 | commit-message: "chore: Update pops formula to ${{ env.TAG_NAME }}" 101 | title: "Update pops formula to ${{ env.TAG_NAME }}" 102 | body: "This PR was automatically created by GitHub Actions to bump pops to ${{ env.TAG_NAME }}." 103 | base: main 104 | branch: update-pops-${{ env.TAG_NAME }} 105 | delete-branch: true 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | *.test 8 | *.out 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.coverprofile 12 | 13 | # Output of the go build tool 14 | bin/ 15 | build/ 16 | 17 | # Dependency directories (remove the comment below if you use Go modules) 18 | # /vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | # IDE and editor specific files 24 | .vscode/ 25 | .idea/ 26 | *.swp 27 | 28 | # OS-specific files 29 | .DS_Store 30 | Thumbs.db 31 | 32 | # Environment variables file 33 | .env 34 | .env.local 35 | 36 | # dist directory 37 | dist/ 38 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners are the maintainers and approvers of this repo 2 | 3 | - @ytimocin 4 | - @prompt-ops/maintainers 5 | - @prompt-ops/approvers 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Prompt-Ops Contribution Guide 2 | 3 | ## How to Contribute 4 | 5 | 1. Fork the repository. 6 | 2. Create a feature branch (`git checkout -b feature/my-feature`). 7 | 3. Commit your changes (`git commit -m "Add feature"`). 8 | 4. Push to your branch (`git push origin feature/my-feature`). 9 | 5. Open a pull request! 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Include all make files in the make directory 2 | include $(wildcard make/*.mk) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤖 Prompt-Ops 2 | 3 | **Prompt-Ops** is a CLI tool that makes managing your infrastructure—like Kubernetes clusters, databases, and cloud environments—effortless and intuitive. By translating natural language into precise commands, it eliminates the need to memorize complex syntax or juggle multiple tools. 4 | 5 | With features like interactive flows, intelligent suggestions, and broad connection support, Prompt-Ops streamlines operations, saves time, and makes managing complex systems more approachable. 6 | 7 | ## Table of Contents 8 | 9 | - [🤖 Prompt-Ops](#-prompt-ops) 10 | - [Table of Contents](#table-of-contents) 11 | - [🚀 Key Features](#-key-features) 12 | - [🛠️ Installation](#️-installation) 13 | - [Using Curl](#using-curl) 14 | - [Using Homebrew (WIP)](#using-homebrew-wip) 15 | - [Using Make](#using-make) 16 | - [🎮 Usage](#-usage) 17 | - [📜 Available Commands](#-available-commands) 18 | - [🌍 General](#-general) 19 | - [🌥️ Cloud](#️-cloud) 20 | - [🚆 Kubernetes](#-kubernetes) 21 | - [💿 Database](#-database) 22 | - [〄 Supported Connection Types](#-supported-connection-types) 23 | - [Available Now](#available-now) 24 | - [Coming Soon](#coming-soon) 25 | - [🎯 Planned Features](#-planned-features) 26 | - [🤝 Contributing](#-contributing) 27 | - [🪪 License](#-license) 28 | - [📚 Examples](#-examples) 29 | 30 | ## 🚀 Key Features 31 | 32 | - 🔍 **Natural Language Commands**: Interact with your services using plain English. 33 | - ⚡ **Interactive Workflows**: Step-by-step prompts for setup and operations. 34 | - 🌐 **Broad Compatibility**: Supports Kubernetes, databases, cloud services, and more. 35 | - 🔮 **AI-Powered Suggestions**: Get guided next steps and smart command completions. 36 | 37 | ## 🛠️ Installation 38 | 39 | You can install **Prompt-Ops** using one of the following methods: 40 | 41 | ### Using Curl 42 | 43 | Run the installation script using **curl**: 44 | 45 | ```bash 46 | curl -fsSL https://raw.githubusercontent.com/prompt-ops/pops/main/scripts/install.sh | bash 47 | ``` 48 | 49 | ### Using Homebrew (WIP) 50 | 51 | You can also install Prompt-Ops via Homebrew: 52 | 53 | ```bash 54 | brew tap prompt-ops/homebrew-tap 55 | brew install pops 56 | ``` 57 | 58 | ### Using Make 59 | 60 | To install locally using `make`: 61 | 62 | ```bash 63 | make install 64 | ``` 65 | 66 | ## 🎮 Usage 67 | 68 | You need to have `OPENAI_API_KEY` in the environment variables to be able to run certain features of Prompt-Ops. You can set it as follows: 69 | 70 | ```bash 71 | export OPENAI_API_KEY=your_api_key_here 72 | ``` 73 | 74 | ## 📜 Available Commands 75 | 76 | ### 🌍 General 77 | 78 | - `pops conn create`: Create a new connection interactively. 79 | - `pops conn list`: List all connections. 80 | - `pops conn open [conn-name]`: Open a specific connection. 81 | - `pops conn delete [conn-name]`: Delete a specific connection. 82 | - `pops conn types`: Show available connection types. 83 | 84 | ### 🌥️ Cloud 85 | 86 | - `pops conn cloud create`: Create a cloud connection interactively. 87 | - `pops conn cloud list`: List all cloud connections. 88 | - `pops conn cloud open [conn-name]`: Open a specific cloud connection. 89 | - `pops conn cloud delete [conn-name]`: Delete a specific cloud connection. 90 | - `pops conn cloud types`: Show supported cloud providers. 91 | 92 | ### 🚆 Kubernetes 93 | 94 | - `pops conn kubernetes create`: Create a Kubernetes connection. 95 | - `pops conn kubernetes list`: List Kubernetes connections. 96 | - `pops conn kubernetes open [conn-name]`: Open a specific Kubernetes connection. 97 | - `pops conn kubernetes delete [conn-name]`: Delete a Kubernetes connection. 98 | 99 | ### 💿 Database 100 | 101 | - `pops conn db create`: Create a database connection. 102 | - `pops conn db list`: List database connections. 103 | - `pops conn db open [conn-name]`: Open a specific database connection. 104 | - `pops conn db delete [conn-name]`: Delete a database connection. 105 | - `pops conn db types`: Show supported database types. 106 | 107 | ## 〄 Supported Connection Types 108 | 109 | ### Available Now 110 | 111 | - **Kubernetes** 112 | - **Databases**: 113 | - PostgreSQL 114 | - MySQL 115 | - MongoDB 116 | - **Cloud**: 117 | - Azure 118 | 119 | ### Coming Soon 120 | 121 | - **Cloud Providers**: AWS, GCP 122 | - **Message Queues**: Kafka, RabbitMQ, AWS SQS 123 | - **Object Storage**: AWS S3, Azure Blob, GCP Storage 124 | - **Monitoring & Logging**: Prometheus, Elasticsearch, Datadog, Splunk 125 | - **CI/CD**: Jenkins, GitLab CI, GitHub Actions, CircleCI 126 | - **Cache**: Redis, Memcached 127 | 128 | ## 🎯 Planned Features 129 | 130 | - **Message Queues**: pops connection mq for Kafka, RabbitMQ. 131 | - **Storage**: pops connection storage for object storage (e.g., S3, Azure Blob). 132 | - **Monitoring**: pops connection monitoring for logging and metrics (e.g., Prometheus). 133 | - **Sessions**: Keep track of prompts, commands, and history. 134 | - **CI/CD Pipelines**: Integrations with popular tools like Jenkins and GitHub Actions. 135 | 136 | ## 🤝 Contributing 137 | 138 | We welcome contributions! Please see our [CONTRIBUTING.md](docs/contributing/CONTRIBUTING.md) for guidelines on how to get started. 139 | 140 | ## 🪪 License 141 | 142 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 143 | 144 | ## 📚 Examples 145 | 146 | Please see [Prompt-Ops examples](docs/examples/README.md) for details. 147 | -------------------------------------------------------------------------------- /cmd/docgen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/prompt-ops/pops/cmd/pops/app" 12 | 13 | "github.com/spf13/cobra/doc" 14 | ) 15 | 16 | func main() { 17 | if len(os.Args) != 2 { 18 | log.Fatal("usage: go run cmd/docgen/main.go ") 19 | } 20 | 21 | output := os.Args[1] 22 | _, err := os.Stat(output) 23 | if os.IsNotExist(err) { 24 | err = os.Mkdir(output, 0755) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | } else if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | err = doc.GenMarkdownTreeCustom(app.NewRootCommand(), output, frontmatter, link) 33 | if err != nil { 34 | log.Fatal(err) //nolint:forbidigo // this is OK inside the main function. 35 | } 36 | } 37 | 38 | const template = `--- 39 | type: docs 40 | title: "%s CLI reference" 41 | linkTitle: "%s" 42 | slug: %s 43 | url: %s 44 | description: "Details on the %s Prompt-Ops CLI command" 45 | --- 46 | ` 47 | 48 | func frontmatter(filename string) string { 49 | name := filepath.Base(filename) 50 | base := strings.TrimSuffix(name, path.Ext(name)) 51 | command := strings.Replace(base, "_", " ", -1) 52 | url := "/reference/cli/" + strings.ToLower(base) + "/" 53 | return fmt.Sprintf(template, command, command, base, url, command) 54 | } 55 | 56 | func link(name string) string { 57 | base := strings.TrimSuffix(name, path.Ext(name)) 58 | return "{{< ref " + strings.ToLower(base) + ".md >}}" 59 | } 60 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/cloud/cloud.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewRootCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "cloud", 10 | Short: "Manage cloud provider connections.", 11 | Long: ` 12 | Cloud Connection: 13 | 14 | - Available Cloud connection types: Azure. 15 | - Commands: create, delete, open, list, types. 16 | - Examples: 17 | * 'pops conn cloud create' creates a connection to a cloud provider. 18 | * 'pops conn cloud open' opens an existing cloud connection. 19 | * 'pops conn cloud list' lists all cloud connections. 20 | * 'pops conn cloud delete' deletes a cloud connection. 21 | * 'pops conn cloud types' lists all available cloud connection types (for now; Azure). 22 | 23 | More connection types and features are coming soon!`, 24 | } 25 | 26 | // `pops connection cloud create *` commands 27 | cmd.AddCommand(newCreateCmd()) 28 | 29 | // `pops connection cloud open *` commands 30 | cmd.AddCommand(newOpenCmd()) 31 | 32 | // `pops connection cloud list` command 33 | cmd.AddCommand(newListCmd()) 34 | 35 | // `pops connection cloud delete *` commands 36 | cmd.AddCommand(newDeleteCmd()) 37 | 38 | // `pops connection cloud types` command 39 | cmd.AddCommand(newTypesCmd()) 40 | 41 | return cmd 42 | } 43 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/cloud/create.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/prompt-ops/pops/pkg/config" 8 | "github.com/prompt-ops/pops/pkg/conn" 9 | "github.com/prompt-ops/pops/pkg/ui" 10 | "github.com/prompt-ops/pops/pkg/ui/conn/cloud" 11 | "github.com/prompt-ops/pops/pkg/ui/shell" 12 | 13 | tea "github.com/charmbracelet/bubbletea" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | type createModel struct { 18 | current tea.Model 19 | } 20 | 21 | func initialCreateModel() *createModel { 22 | return &createModel{ 23 | current: cloud.NewCreateModel(), 24 | } 25 | } 26 | 27 | // NewCreateModel returns a new createModel 28 | func NewCreateModel() *createModel { 29 | return initialCreateModel() 30 | } 31 | 32 | func (m *createModel) Init() tea.Cmd { 33 | return m.current.Init() 34 | } 35 | 36 | func (m *createModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 37 | switch msg := msg.(type) { 38 | case ui.TransitionToShellMsg: 39 | shell := shell.NewShellModel(msg.Connection) 40 | return shell, shell.Init() 41 | } 42 | var cmd tea.Cmd 43 | m.current, cmd = m.current.Update(msg) 44 | return m, cmd 45 | } 46 | 47 | func (m *createModel) View() string { 48 | return m.current.View() 49 | } 50 | 51 | func newCreateCmd() *cobra.Command { 52 | var name string 53 | var provider string 54 | 55 | cmd := &cobra.Command{ 56 | Use: "create", 57 | Short: "Create a new cloud connection.", 58 | Long: ` 59 | Cloud Connection: 60 | 61 | - Available Cloud connection types: Azure. 62 | - Commands: create, delete, open, list, types. 63 | - Examples: 64 | * 'pops conn cloud create' creates a connection interactively. 65 | * 'pops conn cloud create --name my-azure-conn --provider azure' creates a connection non-interactively. 66 | `, 67 | Run: func(cmd *cobra.Command, args []string) { 68 | // Non-interactive mode 69 | if name != "" && provider != "" { 70 | err := createCloudConnection(name, provider) 71 | if err != nil { 72 | fmt.Printf("Error creating cloud connection: %v\n", err) 73 | return 74 | } 75 | 76 | transitionMsg := ui.TransitionToShellMsg{ 77 | Connection: conn.NewCloudConnection(name, 78 | conn.AvailableCloudConnectionType{ 79 | Subtype: strings.Title(provider), 80 | }, 81 | ), 82 | } 83 | 84 | p := tea.NewProgram(initialCreateModel()) 85 | 86 | // Trying to send the transition message before we start the loop. 87 | go func() { 88 | p.Send(transitionMsg) 89 | }() 90 | 91 | if _, err := p.Run(); err != nil { 92 | fmt.Printf("Error transitioning to shell: %v\n", err) 93 | } 94 | } else { 95 | // Interactive mode 96 | p := tea.NewProgram(initialCreateModel()) 97 | if _, err := p.Run(); err != nil { 98 | fmt.Printf("Error running interactive mode: %v\n", err) 99 | } 100 | } 101 | }, 102 | } 103 | 104 | cmd.Flags().StringVar(&name, "name", "", "Name of the cloud connection") 105 | cmd.Flags().StringVar(&provider, "provider", "", "Cloud provider (azure, aws, gcp)") 106 | 107 | return cmd 108 | } 109 | 110 | func createCloudConnection(name, provider string) error { 111 | name = strings.TrimSpace(name) 112 | provider = strings.ToLower(strings.TrimSpace(provider)) 113 | 114 | if name == "" { 115 | return fmt.Errorf("connection name cannot be empty") 116 | } 117 | 118 | var selectedProvider conn.AvailableCloudConnectionType 119 | for _, p := range conn.AvailableCloudConnectionTypes { 120 | if strings.ToLower(p.Subtype) == provider { 121 | selectedProvider = p 122 | break 123 | } 124 | } 125 | if selectedProvider.Subtype == "" { 126 | return fmt.Errorf("unsupported cloud provider: %s", provider) 127 | } 128 | 129 | if config.CheckIfNameExists(name) { 130 | return fmt.Errorf("connection name '%s' already exists", name) 131 | } 132 | 133 | connection := conn.NewCloudConnection(name, selectedProvider) 134 | if err := config.SaveConnection(connection); err != nil { 135 | return fmt.Errorf("failed to save connection: %w", err) 136 | } 137 | 138 | fmt.Printf("✅ Cloud connection '%s' created successfully with provider '%s'.\n", name, selectedProvider.Subtype) 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/cloud/delete.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/prompt-ops/pops/pkg/config" 7 | "github.com/prompt-ops/pops/pkg/conn" 8 | "github.com/prompt-ops/pops/pkg/ui" 9 | 10 | "github.com/charmbracelet/bubbles/table" 11 | tea "github.com/charmbracelet/bubbletea" 12 | "github.com/charmbracelet/lipgloss" 13 | "github.com/fatih/color" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // newDeleteCmd creates the delete command 18 | func newDeleteCmd() *cobra.Command { 19 | var name string 20 | 21 | deleteCmd := &cobra.Command{ 22 | Use: "delete [connection-name]", 23 | Short: "Delete a cloud connection or all cloud connections", 24 | Long: `Delete a cloud connection or all cloud connections. 25 | 26 | You can specify the connection name either as a positional argument or using the --name flag. 27 | 28 | Examples: 29 | pops connection cloud delete my-cloud-connection 30 | pops connection cloud delete --name my-cloud-connection 31 | pops connection cloud delete --all 32 | `, 33 | Args: cobra.MaximumNArgs(1), 34 | Run: func(cmd *cobra.Command, args []string) { 35 | all, err := cmd.Flags().GetBool("all") 36 | if err != nil { 37 | color.Red("Error parsing flags: %v", err) 38 | return 39 | } 40 | 41 | if all { 42 | // If --all flag is provided, ignore other arguments and flags 43 | err := ui.RunWithSpinner("Deleting all cloud connections...", deleteAllCloudConnections) 44 | if err != nil { 45 | color.Red("Failed to delete all cloud connections: %v", err) 46 | } 47 | return 48 | } 49 | 50 | var connectionName string 51 | 52 | // Determine the connection name based on --name flag and positional arguments 53 | if name != "" && len(args) > 0 { 54 | // If both --name flag and positional argument are provided, prioritize the flag 55 | fmt.Println("Warning: --name flag is provided; ignoring positional argument.") 56 | connectionName = name 57 | } else if name != "" { 58 | // If only --name flag is provided 59 | connectionName = name 60 | } else if len(args) == 1 { 61 | // If only positional argument is provided 62 | connectionName = args[0] 63 | } else { 64 | // Interactive mode if neither --name flag nor positional argument is provided 65 | selectedConnection, err := runInteractiveDelete() 66 | if err != nil { 67 | color.Red("Error: %v", err) 68 | return 69 | } 70 | if selectedConnection != "" { 71 | err := ui.RunWithSpinner(fmt.Sprintf("Deleting cloud connection '%s'...", selectedConnection), func() error { 72 | return deleteCloudConnection(selectedConnection) 73 | }) 74 | if err != nil { 75 | color.Red("Failed to delete cloud connection '%s': %v", selectedConnection, err) 76 | } 77 | } 78 | return 79 | } 80 | 81 | // Non-interactive mode: Delete the specified connection 82 | err = ui.RunWithSpinner(fmt.Sprintf("Deleting cloud connection '%s'...", connectionName), func() error { 83 | return deleteCloudConnection(connectionName) 84 | }) 85 | if err != nil { 86 | color.Red("Failed to delete cloud connection '%s': %v", connectionName, err) 87 | } 88 | }, 89 | } 90 | 91 | // Define the --name flag 92 | deleteCmd.Flags().StringVar(&name, "name", "", "Name of the cloud connection to delete") 93 | // Define the --all flag 94 | deleteCmd.Flags().Bool("all", false, "Delete all cloud connections") 95 | 96 | return deleteCmd 97 | } 98 | 99 | // deleteAllCloudConnections deletes all cloud connections 100 | func deleteAllCloudConnections() error { 101 | if err := config.DeleteAllConnectionsByType(conn.ConnectionTypeCloud); err != nil { 102 | return fmt.Errorf("error deleting all cloud connections: %w", err) 103 | } 104 | color.Green("All cloud connections have been successfully deleted.") 105 | return nil 106 | } 107 | 108 | // deleteCloudConnection deletes a single cloud connection by name 109 | func deleteCloudConnection(name string) error { 110 | // Check if the connection exists before attempting to delete 111 | conn, err := getConnectionByName(name) 112 | if err != nil { 113 | return fmt.Errorf("connection '%s' does not exist", name) 114 | } 115 | 116 | if err := config.DeleteConnectionByName(name); err != nil { 117 | return fmt.Errorf("error deleting cloud connection: %w", err) 118 | } 119 | 120 | color.Green("Cloud connection '%s' has been successfully deleted.", conn.Name) 121 | return nil 122 | } 123 | 124 | // runInteractiveDelete runs the Bubble Tea program for interactive deletion 125 | func runInteractiveDelete() (string, error) { 126 | connections, err := config.GetConnectionsByType(conn.ConnectionTypeCloud) 127 | if err != nil { 128 | return "", fmt.Errorf("getting connections: %w", err) 129 | } 130 | 131 | if len(connections) == 0 { 132 | return "", fmt.Errorf("no cloud connections available to delete") 133 | } 134 | 135 | items := make([]table.Row, len(connections)) 136 | for i, conn := range connections { 137 | items[i] = table.Row{conn.Name, conn.Type.GetMainType(), conn.Type.GetSubtype()} 138 | } 139 | 140 | columns := []table.Column{ 141 | {Title: "Name", Width: 25}, 142 | {Title: "Type", Width: 15}, 143 | {Title: "Driver", Width: 20}, 144 | } 145 | 146 | t := table.New( 147 | table.WithColumns(columns), 148 | table.WithRows(items), 149 | table.WithFocused(true), 150 | table.WithHeight(10), 151 | ) 152 | 153 | s := table.DefaultStyles() 154 | s.Header = s.Header. 155 | BorderStyle(lipgloss.NormalBorder()). 156 | BorderForeground(lipgloss.Color("240")). 157 | BorderBottom(true). 158 | Bold(false) 159 | s.Selected = s.Selected. 160 | Foreground(lipgloss.Color("0")). 161 | Background(lipgloss.Color("212")). 162 | Bold(true) 163 | t.SetStyles(s) 164 | 165 | deleteTableModel := ui.NewTableModel(t, nil, false) 166 | 167 | p := tea.NewProgram(deleteTableModel) 168 | if _, err := p.Run(); err != nil { 169 | return "", fmt.Errorf("running Bubble Tea program: %w", err) 170 | } 171 | 172 | return deleteTableModel.Selected(), nil 173 | } 174 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/cloud/list.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/prompt-ops/pops/pkg/config" 8 | "github.com/prompt-ops/pops/pkg/conn" 9 | "github.com/prompt-ops/pops/pkg/ui" 10 | 11 | "github.com/charmbracelet/bubbles/table" 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/charmbracelet/lipgloss" 14 | "github.com/fatih/color" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | func newListCmd() *cobra.Command { 19 | listCmd := &cobra.Command{ 20 | Use: "list", 21 | Short: "List all cloud connections", 22 | Long: "List all cloud connections that have been set up.", 23 | Run: func(cmd *cobra.Command, args []string) { 24 | if err := runListConnections(); err != nil { 25 | color.Red("Error listing cloud connections: %v", err) 26 | os.Exit(1) 27 | } 28 | }, 29 | } 30 | 31 | return listCmd 32 | } 33 | 34 | // runListConnections lists all connections 35 | func runListConnections() error { 36 | connections, err := config.GetConnectionsByType(conn.ConnectionTypeCloud) 37 | if err != nil { 38 | return fmt.Errorf("getting cloud connections: %w", err) 39 | } 40 | 41 | items := make([]table.Row, len(connections)) 42 | for i, conn := range connections { 43 | items[i] = table.Row{conn.Name, conn.Type.GetMainType(), conn.Type.GetSubtype()} 44 | } 45 | 46 | columns := []table.Column{ 47 | {Title: "Name", Width: 25}, 48 | {Title: "Type", Width: 15}, 49 | {Title: "Subtype", Width: 20}, 50 | } 51 | 52 | t := table.New( 53 | table.WithColumns(columns), 54 | table.WithRows(items), 55 | table.WithFocused(true), 56 | table.WithHeight(10), 57 | ) 58 | 59 | s := table.DefaultStyles() 60 | s.Header = s.Header. 61 | BorderStyle(lipgloss.NormalBorder()). 62 | BorderForeground(lipgloss.Color("240")). 63 | BorderBottom(true). 64 | Bold(false) 65 | s.Selected = s.Selected. 66 | Foreground(lipgloss.Color("0")). 67 | Background(lipgloss.Color("212")). 68 | Bold(true) 69 | t.SetStyles(s) 70 | 71 | openTableModel := ui.NewTableModel(t, nil, true) 72 | 73 | p := tea.NewProgram(openTableModel) 74 | if _, err := p.Run(); err != nil { 75 | panic(err) 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/cloud/open.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/prompt-ops/pops/pkg/config" 7 | "github.com/prompt-ops/pops/pkg/conn" 8 | "github.com/prompt-ops/pops/pkg/ui" 9 | "github.com/prompt-ops/pops/pkg/ui/conn/cloud" 10 | "github.com/prompt-ops/pops/pkg/ui/shell" 11 | 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | type openModel struct { 17 | current tea.Model 18 | } 19 | 20 | func initialOpenModel() *openModel { 21 | return &openModel{ 22 | current: cloud.NewOpenModel(), 23 | } 24 | } 25 | 26 | // NewOpenModel returns a new openModel 27 | func NewOpenModel() *openModel { 28 | return initialOpenModel() 29 | } 30 | 31 | func (m *openModel) Init() tea.Cmd { 32 | return m.current.Init() 33 | } 34 | 35 | func (m *openModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 36 | switch msg := msg.(type) { 37 | case ui.TransitionToShellMsg: 38 | shell := shell.NewShellModel(msg.Connection) 39 | return shell, shell.Init() 40 | } 41 | var cmd tea.Cmd 42 | m.current, cmd = m.current.Update(msg) 43 | return m, cmd 44 | } 45 | 46 | func (m *openModel) View() string { 47 | return m.current.View() 48 | } 49 | 50 | func newOpenCmd() *cobra.Command { 51 | var name string 52 | 53 | cmd := &cobra.Command{ 54 | Use: "open [connection-name]", 55 | Short: "Open an existing cloud conn.", 56 | Long: `Open a cloud connection to access its shell. 57 | 58 | You can specify the connection name either as a positional argument or using the --name flag. 59 | 60 | Examples: 61 | pops connection cloud open my-azure-conn 62 | pops connection cloud open --name my-azure-conn 63 | `, 64 | Args: cobra.MaximumNArgs(1), 65 | Run: func(cmd *cobra.Command, args []string) { 66 | var connectionName string 67 | 68 | // Determine the connection name based on flag and arguments 69 | if name != "" && len(args) > 0 { 70 | // If both flag and argument are provided, prioritize the flag 71 | fmt.Println("Warning: --name flag is provided; ignoring positional argument.") 72 | connectionName = name 73 | } else if name != "" { 74 | // If only flag is provided 75 | connectionName = name 76 | } else if len(args) == 1 { 77 | // If only positional argument is provided 78 | connectionName = args[0] 79 | } else { 80 | // Interactive mode if neither flag nor argument is provided 81 | p := tea.NewProgram(initialOpenModel()) 82 | if _, err := p.Run(); err != nil { 83 | fmt.Printf("Error running interactive mode: %v\n", err) 84 | } 85 | return 86 | } 87 | 88 | // Non-interactive mode: Open the specified connection 89 | conn, err := getConnectionByName(connectionName) 90 | if err != nil { 91 | fmt.Printf("Error: %v\n", err) 92 | return 93 | } 94 | 95 | transitionMsg := ui.TransitionToShellMsg{ 96 | Connection: conn, 97 | } 98 | 99 | p := tea.NewProgram(initialOpenModel()) 100 | 101 | // Send the transition message before running the program 102 | go func() { 103 | p.Send(transitionMsg) 104 | }() 105 | 106 | if _, err := p.Run(); err != nil { 107 | fmt.Printf("Error transitioning to shell: %v\n", err) 108 | } 109 | }, 110 | } 111 | 112 | // Define the --name flag 113 | cmd.Flags().StringVar(&name, "name", "", "Name of the cloud connection") 114 | 115 | return cmd 116 | } 117 | 118 | // getConnectionByName retrieves a cloud connection by its name. 119 | // Returns an error if the connection does not exist. 120 | func getConnectionByName(name string) (conn.Connection, error) { 121 | cloudConnections, err := config.GetConnectionsByType(conn.ConnectionTypeCloud) 122 | if err != nil { 123 | return conn.Connection{}, fmt.Errorf("failed to retrieve connections: %w", err) 124 | } 125 | 126 | for _, conn := range cloudConnections { 127 | if conn.Name == name { 128 | return conn, nil 129 | } 130 | } 131 | 132 | return conn.Connection{}, fmt.Errorf("connection '%s' does not exist", name) 133 | } 134 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/cloud/types.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/prompt-ops/pops/pkg/conn" 7 | "github.com/prompt-ops/pops/pkg/ui" 8 | 9 | "github.com/charmbracelet/bubbles/table" 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | "github.com/fatih/color" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func newTypesCmd() *cobra.Command { 17 | listCmd := &cobra.Command{ 18 | Use: "types", 19 | Short: "List all available cloud connection types", 20 | Long: "List all available cloud connection types", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | if err := runListAvaibleCloudTypes(); err != nil { 23 | color.Red("Error listing cloud connections: %v", err) 24 | os.Exit(1) 25 | } 26 | }, 27 | } 28 | 29 | return listCmd 30 | } 31 | 32 | // runListAvaibleCloudTypes lists all available cloud connection types 33 | func runListAvaibleCloudTypes() error { 34 | cloudConnectionTypes := conn.AvailableCloudConnectionTypes 35 | 36 | items := make([]table.Row, len(cloudConnectionTypes)) 37 | for i, cloudConnectionType := range cloudConnectionTypes { 38 | items[i] = table.Row{cloudConnectionType.Subtype} 39 | } 40 | 41 | columns := []table.Column{ 42 | {Title: "Available Types", Width: 25}, 43 | } 44 | 45 | t := table.New( 46 | table.WithColumns(columns), 47 | table.WithRows(items), 48 | table.WithFocused(true), 49 | table.WithHeight(10), 50 | ) 51 | 52 | s := table.DefaultStyles() 53 | s.Header = s.Header. 54 | BorderStyle(lipgloss.NormalBorder()). 55 | BorderForeground(lipgloss.Color("240")). 56 | BorderBottom(true). 57 | Bold(false) 58 | s.Selected = s.Selected. 59 | Foreground(lipgloss.Color("0")). 60 | Background(lipgloss.Color("212")). 61 | Bold(true) 62 | t.SetStyles(s) 63 | 64 | openTableModel := ui.NewTableModel(t, nil, true) 65 | 66 | p := tea.NewProgram(openTableModel) 67 | if _, err := p.Run(); err != nil { 68 | panic(err) 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/connection.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "github.com/prompt-ops/pops/cmd/pops/app/conn/cloud" 5 | "github.com/prompt-ops/pops/cmd/pops/app/conn/db" 6 | "github.com/prompt-ops/pops/cmd/pops/app/conn/k8s" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // NewConnectionCommand creates the 'connection' command with descriptions and examples for managing connections. 12 | func NewConnectionCommand() *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "connection", 15 | Aliases: []string{"conn"}, 16 | Short: "Manage your infrastructure connections using natural language.", 17 | Long: ` 18 | Prompt-Ops manages your infrastructure using natural language. 19 | 20 | **Cloud Connection:** 21 | - **Types**: Azure, AWS, and GCP (coming soon) 22 | - **Commands**: create, delete, open, list, types 23 | - **Example**: 'pops connection cloud create' creates a connection to a cloud provider. 24 | 25 | **Database Connection:** 26 | - **Types**: MySQL, PostgreSQL, MongoDB 27 | - **Commands**: create, delete, open, list, types 28 | - **Example**: 'pops connection db create' creates a connection to a database. 29 | 30 | **Kubernetes Connection:** 31 | - **Types**: Any available Kubernetes cluster 32 | - **Commands**: create, delete, open, list, types 33 | - **Example**: 'pops connection kubernetes create' creates a connection to a Kubernetes cluster. 34 | 35 | More connection types and features are coming soon!`, 36 | Example: ` 37 | - **pops connection create** - Create a connection by selecting from available types. 38 | - **pops connection open** - Open a connection by selecting from available connections. 39 | - **pops connection delete** - Delete a connection by selecting from available connections. 40 | - **pops connection delete --all** - Delete all available connections. 41 | - **pops connection list** - List all available connections. 42 | `, 43 | } 44 | 45 | // Add subcommands 46 | cmd.AddCommand(cloud.NewRootCommand()) 47 | cmd.AddCommand(k8s.NewRootCommand()) 48 | cmd.AddCommand(db.NewRootCommand()) 49 | 50 | // Add additional commands 51 | cmd.AddCommand(newListCmd()) 52 | cmd.AddCommand(newDeleteCmd()) 53 | cmd.AddCommand(newOpenCmd()) 54 | cmd.AddCommand(newCreateCmd()) 55 | cmd.AddCommand(newTypesCmd()) 56 | 57 | return cmd 58 | } 59 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/create.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "github.com/prompt-ops/pops/cmd/pops/app/conn/factory" 5 | "github.com/prompt-ops/pops/pkg/conn" 6 | "github.com/prompt-ops/pops/pkg/ui" 7 | 8 | "github.com/charmbracelet/bubbles/table" 9 | tea "github.com/charmbracelet/bubbletea" 10 | "github.com/charmbracelet/lipgloss" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func newCreateCmd() *cobra.Command { 15 | createCmd := &cobra.Command{ 16 | Use: "create", 17 | Short: "Create a new connection", 18 | Long: "Create a new connection", 19 | Example: `pops connection create`, 20 | Run: func(cmd *cobra.Command, args []string) { 21 | // `pops connection create` interactive command. 22 | // This command will be used to create a new connection. 23 | runInteractiveCreate() 24 | }, 25 | } 26 | 27 | return createCmd 28 | } 29 | 30 | func runInteractiveCreate() { 31 | m := initialCreateModel() 32 | p := tea.NewProgram(m) 33 | if _, err := p.Run(); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | type createConnectionStep int 39 | 40 | const ( 41 | createStepTypeSelection createConnectionStep = iota 42 | createStepCreateModel 43 | ) 44 | 45 | type createModel struct { 46 | currentStep createConnectionStep 47 | typeSelectionModel tea.Model 48 | createModel tea.Model 49 | } 50 | 51 | func initialCreateModel() *createModel { 52 | connectionTypes := conn.AvailableConnectionTypes() 53 | 54 | items := make([]table.Row, len(connectionTypes)) 55 | for i, connectionType := range connectionTypes { 56 | items[i] = table.Row{ 57 | connectionType, 58 | } 59 | } 60 | 61 | columns := []table.Column{ 62 | {Title: "Type", Width: 25}, 63 | } 64 | 65 | t := table.New( 66 | table.WithColumns(columns), 67 | table.WithRows(items), 68 | table.WithFocused(true), 69 | table.WithHeight(10), 70 | ) 71 | 72 | s := table.DefaultStyles() 73 | s.Header = s.Header. 74 | BorderStyle(lipgloss.NormalBorder()). 75 | BorderForeground(lipgloss.Color("240")). 76 | BorderBottom(true). 77 | Bold(false) 78 | s.Selected = s.Selected. 79 | Foreground(lipgloss.Color("0")). 80 | Background(lipgloss.Color("212")). 81 | Bold(true) 82 | t.SetStyles(s) 83 | 84 | onSelect := func(selectedType string) tea.Msg { 85 | return ui.TransitionToCreateMsg{ 86 | ConnectionType: selectedType, 87 | } 88 | } 89 | 90 | typeSelectionModel := ui.NewTableModel(t, onSelect, false) 91 | 92 | return &createModel{ 93 | currentStep: createStepTypeSelection, 94 | typeSelectionModel: typeSelectionModel, 95 | } 96 | } 97 | 98 | func (m *createModel) Init() tea.Cmd { 99 | return m.typeSelectionModel.Init() 100 | } 101 | 102 | func (m *createModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 103 | switch m.currentStep { 104 | case createStepTypeSelection: 105 | switch msg := msg.(type) { 106 | case ui.TransitionToCreateMsg: 107 | connectionType := msg.ConnectionType 108 | createModel, err := factory.GetCreateModel(connectionType) 109 | if err != nil { 110 | return m, tea.Quit 111 | } 112 | 113 | m.currentStep = createStepCreateModel 114 | m.createModel = createModel 115 | return m, createModel.Init() 116 | default: 117 | var cmd tea.Cmd 118 | m.typeSelectionModel, cmd = m.typeSelectionModel.Update(msg) 119 | return m, cmd 120 | } 121 | case createStepCreateModel: 122 | var cmd tea.Cmd 123 | m.createModel, cmd = m.createModel.Update(msg) 124 | return m, cmd 125 | default: 126 | return m, tea.Quit 127 | } 128 | } 129 | 130 | func (m *createModel) View() string { 131 | switch m.currentStep { 132 | case createStepTypeSelection: 133 | return m.typeSelectionModel.View() 134 | case createStepCreateModel: 135 | return m.createModel.View() 136 | default: 137 | return "Unknown step" 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/db/create.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/prompt-ops/pops/pkg/ui" 5 | "github.com/prompt-ops/pops/pkg/ui/conn/db" 6 | "github.com/prompt-ops/pops/pkg/ui/shell" 7 | 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type createModel struct { 13 | current tea.Model 14 | } 15 | 16 | func initialCreateModel() *createModel { 17 | return &createModel{ 18 | current: db.NewCreateModel(), 19 | } 20 | } 21 | 22 | // NewCreateModel returns a new createModel 23 | func NewCreateModel() *createModel { 24 | return initialCreateModel() 25 | } 26 | 27 | func (m *createModel) Init() tea.Cmd { 28 | return m.current.Init() 29 | } 30 | 31 | func (m *createModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 32 | switch msg := msg.(type) { 33 | case ui.TransitionToShellMsg: 34 | shell := shell.NewShellModel(msg.Connection) 35 | return shell, shell.Init() 36 | } 37 | var cmd tea.Cmd 38 | m.current, cmd = m.current.Update(msg) 39 | return m, cmd 40 | } 41 | 42 | func (m *createModel) View() string { 43 | return m.current.View() 44 | } 45 | 46 | func newCreateCmd() *cobra.Command { 47 | cmd := &cobra.Command{ 48 | Use: "create", 49 | Short: "Create a new cloud connection.", 50 | Run: func(cmd *cobra.Command, args []string) { 51 | p := tea.NewProgram(initialCreateModel()) 52 | if _, err := p.Run(); err != nil { 53 | panic(err) 54 | } 55 | }, 56 | } 57 | return cmd 58 | } 59 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewRootCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "db", 10 | Short: "Manage database connections.", 11 | Long: ` 12 | Database Connection: 13 | 14 | - Available Database connection types: MySQL, PostgreSQL, and MongoDB. 15 | - Commands: create, delete, open, list, types. 16 | - Examples: 17 | * 'pops conn db create' creates a connection to a database. 18 | * 'pops conn db open' opens an existing database connection. 19 | * 'pops conn db list' lists all database connections. 20 | * 'pops conn db delete' deletes a database connection. 21 | * 'pops conn db types' lists all available database connection types (for now; MySQL, PostgreSQL, and MongoDB). 22 | 23 | More connection types and features are coming soon!`, 24 | } 25 | 26 | // `pops connection db create *` commands 27 | cmd.AddCommand(newCreateCmd()) 28 | 29 | // `pops connection db open *` commands 30 | cmd.AddCommand(newOpenCmd()) 31 | 32 | // `pops connection db list` command 33 | cmd.AddCommand(newListCmd()) 34 | 35 | // `pops connection db delete *` commands 36 | cmd.AddCommand(newDeleteCmd()) 37 | 38 | // `pops connection db types` command 39 | cmd.AddCommand(newTypesCmd()) 40 | 41 | return cmd 42 | } 43 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/db/delete.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | 6 | config "github.com/prompt-ops/pops/pkg/config" 7 | "github.com/prompt-ops/pops/pkg/conn" 8 | "github.com/prompt-ops/pops/pkg/ui" 9 | 10 | "github.com/charmbracelet/bubbles/table" 11 | tea "github.com/charmbracelet/bubbletea" 12 | "github.com/charmbracelet/lipgloss" 13 | "github.com/fatih/color" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // newDeleteCmd creates the delete command 18 | func newDeleteCmd() *cobra.Command { 19 | deleteCmd := &cobra.Command{ 20 | Use: "delete", 21 | Short: "Delete a database connection or all database connections", 22 | Example: ` 23 | - **pops connection db delete my-db-connection**: Delete a database connection named 'my-db-connection'. 24 | - **pops connection db delete --all**: Delete all database connections. 25 | `, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | all, err := cmd.Flags().GetBool("all") 28 | if err != nil { 29 | color.Red("Error parsing flags: %v", err) 30 | return 31 | } 32 | 33 | if all { 34 | err := ui.RunWithSpinner("Deleting all database connections...", deleteAllDatabaseConnections) 35 | if err != nil { 36 | color.Red("Failed to delete all database connections: %v", err) 37 | } 38 | return 39 | } else if len(args) == 1 { 40 | connectionName := args[0] 41 | err := ui.RunWithSpinner(fmt.Sprintf("Deleting database connection '%s'...", connectionName), func() error { 42 | return deleteDatabaseConnection(connectionName) 43 | }) 44 | if err != nil { 45 | color.Red("Failed to delete database connection '%s': %v", connectionName, err) 46 | } 47 | return 48 | } else { 49 | selectedConnection, err := runInteractiveDelete() 50 | if err != nil { 51 | color.Red("Error: %v", err) 52 | return 53 | } 54 | if selectedConnection != "" { 55 | err := ui.RunWithSpinner(fmt.Sprintf("Deleting database connection '%s'...", selectedConnection), func() error { 56 | return deleteDatabaseConnection(selectedConnection) 57 | }) 58 | if err != nil { 59 | color.Red("Failed to delete database connection '%s': %v", selectedConnection, err) 60 | } 61 | } 62 | } 63 | }, 64 | } 65 | 66 | deleteCmd.Flags().Bool("all", false, "Delete all database connections") 67 | 68 | return deleteCmd 69 | } 70 | 71 | // deleteAllDatabaseConnections deletes all database connections 72 | func deleteAllDatabaseConnections() error { 73 | if err := config.DeleteAllConnectionsByType(conn.ConnectionTypeDatabase); err != nil { 74 | return fmt.Errorf("error deleting all database connections: %w", err) 75 | } 76 | return nil 77 | } 78 | 79 | // deleteDatabaseConnection deletes a single database connection by name 80 | func deleteDatabaseConnection(name string) error { 81 | if err := config.DeleteConnectionByName(name); err != nil { 82 | return fmt.Errorf("error deleting database connection: %w", err) 83 | } 84 | return nil 85 | } 86 | 87 | // runInteractiveDelete runs the Bubble Tea program for interactive deletion 88 | func runInteractiveDelete() (string, error) { 89 | connections, err := config.GetConnectionsByType(conn.ConnectionTypeDatabase) 90 | if err != nil { 91 | return "", fmt.Errorf("getting connections: %w", err) 92 | } 93 | 94 | items := make([]table.Row, len(connections)) 95 | for i, conn := range connections { 96 | items[i] = table.Row{conn.Name, conn.Type.GetMainType(), conn.Type.GetSubtype()} 97 | } 98 | 99 | columns := []table.Column{ 100 | {Title: "Name", Width: 25}, 101 | {Title: "Type", Width: 15}, 102 | {Title: "Driver", Width: 20}, 103 | } 104 | 105 | t := table.New( 106 | table.WithColumns(columns), 107 | table.WithRows(items), 108 | table.WithFocused(true), 109 | table.WithHeight(10), 110 | ) 111 | 112 | s := table.DefaultStyles() 113 | s.Header = s.Header. 114 | BorderStyle(lipgloss.NormalBorder()). 115 | BorderForeground(lipgloss.Color("240")). 116 | BorderBottom(true). 117 | Bold(false) 118 | s.Selected = s.Selected. 119 | Foreground(lipgloss.Color("0")). 120 | Background(lipgloss.Color("212")). 121 | Bold(true) 122 | t.SetStyles(s) 123 | 124 | deleteTableModel := ui.NewTableModel(t, nil, false) 125 | 126 | p := tea.NewProgram(deleteTableModel) 127 | if _, err := p.Run(); err != nil { 128 | return "", fmt.Errorf("running Bubble Tea program: %w", err) 129 | } 130 | 131 | return deleteTableModel.Selected(), nil 132 | } 133 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/db/list.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | config "github.com/prompt-ops/pops/pkg/config" 8 | "github.com/prompt-ops/pops/pkg/conn" 9 | "github.com/prompt-ops/pops/pkg/ui" 10 | 11 | "github.com/charmbracelet/bubbles/table" 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/charmbracelet/lipgloss" 14 | "github.com/fatih/color" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | func newListCmd() *cobra.Command { 19 | listCmd := &cobra.Command{ 20 | Use: "list", 21 | Short: "List all database connections", 22 | Long: "List all database connections that have been set up.", 23 | Run: func(cmd *cobra.Command, args []string) { 24 | if err := runListConnections(); err != nil { 25 | color.Red("Error listing database connections: %v", err) 26 | os.Exit(1) 27 | } 28 | }, 29 | } 30 | 31 | return listCmd 32 | } 33 | 34 | // runListConnections lists all connections 35 | func runListConnections() error { 36 | connections, err := config.GetConnectionsByType(conn.ConnectionTypeDatabase) 37 | if err != nil { 38 | return fmt.Errorf("getting database connections: %w", err) 39 | } 40 | 41 | items := make([]table.Row, len(connections)) 42 | for i, conn := range connections { 43 | items[i] = table.Row{conn.Name, conn.Type.GetMainType(), conn.Type.GetSubtype()} 44 | } 45 | 46 | columns := []table.Column{ 47 | {Title: "Name", Width: 25}, 48 | {Title: "Type", Width: 15}, 49 | {Title: "Driver", Width: 20}, 50 | } 51 | 52 | t := table.New( 53 | table.WithColumns(columns), 54 | table.WithRows(items), 55 | table.WithFocused(true), 56 | table.WithHeight(10), 57 | ) 58 | 59 | s := table.DefaultStyles() 60 | s.Header = s.Header. 61 | BorderStyle(lipgloss.NormalBorder()). 62 | BorderForeground(lipgloss.Color("240")). 63 | BorderBottom(true). 64 | Bold(false) 65 | s.Selected = s.Selected. 66 | Foreground(lipgloss.Color("0")). 67 | Background(lipgloss.Color("212")). 68 | Bold(true) 69 | t.SetStyles(s) 70 | 71 | openTableModel := ui.NewTableModel(t, nil, true) 72 | 73 | p := tea.NewProgram(openTableModel) 74 | if _, err := p.Run(); err != nil { 75 | panic(err) 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/db/open.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | ui "github.com/prompt-ops/pops/pkg/ui" 5 | dbui "github.com/prompt-ops/pops/pkg/ui/conn/db" 6 | "github.com/prompt-ops/pops/pkg/ui/shell" 7 | 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type openModel struct { 13 | current tea.Model 14 | } 15 | 16 | func initialOpenModel() *openModel { 17 | return &openModel{ 18 | current: dbui.NewOpenModel(), 19 | } 20 | } 21 | 22 | // NewOpenModel returns a new openModel 23 | func NewOpenModel() *openModel { 24 | return initialOpenModel() 25 | } 26 | 27 | func (m *openModel) Init() tea.Cmd { 28 | return m.current.Init() 29 | } 30 | 31 | func (m *openModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 32 | switch msg := msg.(type) { 33 | case ui.TransitionToShellMsg: 34 | shell := shell.NewShellModel(msg.Connection) 35 | return shell, shell.Init() 36 | } 37 | var cmd tea.Cmd 38 | m.current, cmd = m.current.Update(msg) 39 | return m, cmd 40 | } 41 | 42 | func (m *openModel) View() string { 43 | return m.current.View() 44 | } 45 | 46 | func newOpenCmd() *cobra.Command { 47 | cmd := &cobra.Command{ 48 | Use: "open", 49 | Short: "Open an existing database connection.", 50 | Run: func(cmd *cobra.Command, args []string) { 51 | p := tea.NewProgram(initialOpenModel()) 52 | if _, err := p.Run(); err != nil { 53 | panic(err) 54 | } 55 | }, 56 | } 57 | return cmd 58 | } 59 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/db/types.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/prompt-ops/pops/pkg/conn" 7 | "github.com/prompt-ops/pops/pkg/ui" 8 | 9 | "github.com/charmbracelet/bubbles/table" 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | "github.com/fatih/color" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func newTypesCmd() *cobra.Command { 17 | listCmd := &cobra.Command{ 18 | Use: "types", 19 | Short: "List all available database connection types", 20 | Long: "List all available database connection types", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | if err := runListAvaibleDatabaseTypes(); err != nil { 23 | color.Red("Error listing database connections: %v", err) 24 | os.Exit(1) 25 | } 26 | }, 27 | } 28 | 29 | return listCmd 30 | } 31 | 32 | // runListAvaibledatabaseTypes lists all available database connection types 33 | func runListAvaibleDatabaseTypes() error { 34 | databaseConnections := conn.AvailableDatabaseConnectionTypes 35 | 36 | items := make([]table.Row, len(databaseConnections)) 37 | for i, connectionType := range databaseConnections { 38 | items[i] = table.Row{connectionType.Subtype} 39 | } 40 | 41 | columns := []table.Column{ 42 | {Title: "Available Types", Width: 25}, 43 | } 44 | 45 | t := table.New( 46 | table.WithColumns(columns), 47 | table.WithRows(items), 48 | table.WithFocused(true), 49 | table.WithHeight(10), 50 | ) 51 | 52 | s := table.DefaultStyles() 53 | s.Header = s.Header. 54 | BorderStyle(lipgloss.NormalBorder()). 55 | BorderForeground(lipgloss.Color("240")). 56 | BorderBottom(true). 57 | Bold(false) 58 | s.Selected = s.Selected. 59 | Foreground(lipgloss.Color("0")). 60 | Background(lipgloss.Color("212")). 61 | Bold(true) 62 | t.SetStyles(s) 63 | 64 | openTableModel := ui.NewTableModel(t, nil, true) 65 | 66 | p := tea.NewProgram(openTableModel) 67 | if _, err := p.Run(); err != nil { 68 | panic(err) 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/delete.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "fmt" 5 | 6 | config "github.com/prompt-ops/pops/pkg/config" 7 | "github.com/prompt-ops/pops/pkg/ui" 8 | 9 | "github.com/charmbracelet/bubbles/table" 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | "github.com/fatih/color" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // newDeleteCmd creates the delete command 17 | func newDeleteCmd() *cobra.Command { 18 | deleteCmd := &cobra.Command{ 19 | Use: "delete", 20 | Short: "Delete a connection or all connections", 21 | Long: "Delete a connection or all connections", 22 | Run: func(cmd *cobra.Command, args []string) { 23 | all, err := cmd.Flags().GetBool("all") 24 | if err != nil { 25 | color.Red("Error parsing flags: %v", err) 26 | return 27 | } 28 | 29 | if all { 30 | err := ui.RunWithSpinner("Deleting all connections...", deleteAllConnections) 31 | if err != nil { 32 | color.Red("Failed to delete all connections: %v", err) 33 | } 34 | return 35 | } else if len(args) == 1 { 36 | connectionName := args[0] 37 | err := ui.RunWithSpinner(fmt.Sprintf("Deleting connection '%s'...", connectionName), func() error { 38 | return deleteConnection(connectionName) 39 | }) 40 | if err != nil { 41 | color.Red("Failed to delete connection '%s': %v", connectionName, err) 42 | } 43 | return 44 | } else { 45 | selectedConnection, err := runInteractiveDelete() 46 | if err != nil { 47 | color.Red("Error: %v", err) 48 | return 49 | } 50 | if selectedConnection != "" { 51 | err := ui.RunWithSpinner(fmt.Sprintf("Deleting connection '%s'...", selectedConnection), func() error { 52 | return deleteConnection(selectedConnection) 53 | }) 54 | if err != nil { 55 | color.Red("Failed to delete connection '%s': %v", selectedConnection, err) 56 | } 57 | } 58 | } 59 | }, 60 | } 61 | 62 | deleteCmd.Flags().Bool("all", false, "Delete all connections") 63 | 64 | return deleteCmd 65 | } 66 | 67 | // deleteAllConnections deletes all connections 68 | func deleteAllConnections() error { 69 | if err := config.DeleteAllConnections(); err != nil { 70 | return fmt.Errorf("error deleting all connections: %w", err) 71 | } 72 | return nil 73 | } 74 | 75 | // deleteConnection deletes a single connection by name 76 | func deleteConnection(name string) error { 77 | if err := config.DeleteConnectionByName(name); err != nil { 78 | return fmt.Errorf("error deleting connection '%s': %w", name, err) 79 | } 80 | return nil 81 | } 82 | 83 | // runInteractiveDelete runs the Bubble Tea program for interactive deletion 84 | func runInteractiveDelete() (string, error) { 85 | connections, err := config.GetAllConnections() 86 | if err != nil { 87 | return "", fmt.Errorf("getting connections: %w", err) 88 | } 89 | 90 | items := make([]table.Row, len(connections)) 91 | for i, conn := range connections { 92 | items[i] = table.Row{conn.Name, conn.Type.GetMainType(), conn.Type.GetSubtype()} 93 | } 94 | 95 | columns := []table.Column{ 96 | {Title: "Name", Width: 15}, 97 | {Title: "Type", Width: 15}, 98 | {Title: "Subtype", Width: 15}, 99 | } 100 | 101 | t := table.New( 102 | table.WithColumns(columns), 103 | table.WithRows(items), 104 | table.WithFocused(true), 105 | table.WithHeight(10), 106 | ) 107 | 108 | s := table.DefaultStyles() 109 | s.Header = s.Header. 110 | BorderStyle(lipgloss.NormalBorder()). 111 | BorderForeground(lipgloss.Color("240")). 112 | BorderBottom(true). 113 | Bold(false) 114 | s.Selected = s.Selected. 115 | Foreground(lipgloss.Color("0")). 116 | Background(lipgloss.Color("212")). 117 | Bold(true) 118 | t.SetStyles(s) 119 | 120 | deleteTableModel := ui.NewTableModel(t, nil, false) 121 | 122 | p := tea.NewProgram(deleteTableModel) 123 | if _, err := p.Run(); err != nil { 124 | return "", fmt.Errorf("running Bubble Tea program: %w", err) 125 | } 126 | 127 | return deleteTableModel.Selected(), nil 128 | } 129 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/factory/create.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/prompt-ops/pops/cmd/pops/app/conn/cloud" 8 | "github.com/prompt-ops/pops/cmd/pops/app/conn/db" 9 | "github.com/prompt-ops/pops/cmd/pops/app/conn/k8s" 10 | 11 | tea "github.com/charmbracelet/bubbletea" 12 | ) 13 | 14 | // GetCreateModel returns a new createModel based on the connection type 15 | func GetCreateModel(connectionType string) (tea.Model, error) { 16 | switch strings.ToLower(connectionType) { 17 | case "cloud": 18 | return cloud.NewCreateModel(), nil 19 | case "kubernetes": 20 | return k8s.NewCreateModel(), nil 21 | case "database": 22 | return db.NewCreateModel(), nil 23 | default: 24 | return nil, fmt.Errorf("[GetCreateModel] unsupported connection type: %s", connectionType) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/factory/doc.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | // This package provides factory functions for creating UI models based on the connection type. 4 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/factory/open.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/prompt-ops/pops/cmd/pops/app/conn/cloud" 8 | "github.com/prompt-ops/pops/cmd/pops/app/conn/db" 9 | "github.com/prompt-ops/pops/cmd/pops/app/conn/k8s" 10 | "github.com/prompt-ops/pops/pkg/conn" 11 | 12 | tea "github.com/charmbracelet/bubbletea" 13 | ) 14 | 15 | // GetOpenModel returns a new openModel based on the connection type 16 | func GetOpenModel(connection conn.Connection) (tea.Model, error) { 17 | switch strings.ToLower(connection.Type.GetMainType()) { 18 | case "cloud": 19 | return cloud.NewOpenModel(), nil 20 | case "kubernetes": 21 | return k8s.NewOpenModel(), nil 22 | case "database": 23 | return db.NewOpenModel(), nil 24 | default: 25 | return nil, fmt.Errorf("[GetOpenModel] unsupported connection type: %s", connection.Type) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/k8s/create.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "github.com/prompt-ops/pops/pkg/ui" 5 | k8sui "github.com/prompt-ops/pops/pkg/ui/conn/k8s" 6 | "github.com/prompt-ops/pops/pkg/ui/shell" 7 | 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type createModel struct { 13 | current tea.Model 14 | } 15 | 16 | func initialCreateModel() *createModel { 17 | return &createModel{ 18 | current: k8sui.NewCreateModel(), 19 | } 20 | } 21 | 22 | // NewCreateModel returns a new createModel 23 | func NewCreateModel() *createModel { 24 | return initialCreateModel() 25 | } 26 | 27 | func (m *createModel) Init() tea.Cmd { 28 | return m.current.Init() 29 | } 30 | 31 | func (m *createModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 32 | switch msg := msg.(type) { 33 | case ui.TransitionToShellMsg: 34 | shell := shell.NewShellModel(msg.Connection) 35 | return shell, shell.Init() 36 | } 37 | var cmd tea.Cmd 38 | m.current, cmd = m.current.Update(msg) 39 | return m, cmd 40 | } 41 | 42 | func (m *createModel) View() string { 43 | return m.current.View() 44 | } 45 | 46 | func newCreateCmd() *cobra.Command { 47 | cmd := &cobra.Command{ 48 | Use: "create", 49 | Short: "Create a new Kubernetes connection.", 50 | Run: func(cmd *cobra.Command, args []string) { 51 | p := tea.NewProgram(initialCreateModel()) 52 | if _, err := p.Run(); err != nil { 53 | panic(err) 54 | } 55 | }, 56 | } 57 | return cmd 58 | } 59 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/k8s/delete.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "fmt" 5 | 6 | config "github.com/prompt-ops/pops/pkg/config" 7 | "github.com/prompt-ops/pops/pkg/conn" 8 | "github.com/prompt-ops/pops/pkg/ui" 9 | 10 | "github.com/charmbracelet/bubbles/table" 11 | tea "github.com/charmbracelet/bubbletea" 12 | "github.com/charmbracelet/lipgloss" 13 | "github.com/fatih/color" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // newDeleteCmd creates the delete command 18 | func newDeleteCmd() *cobra.Command { 19 | deleteCmd := &cobra.Command{ 20 | Use: "delete", 21 | Short: "Delete a kubernetes connection or all kubernetes connections", 22 | Example: ` 23 | - **pops connection kubernetes delete my-k8s-connection**: Delete a kubernetes connection named 'my-k8s-connection'. 24 | - **pops connection kubernetes delete --all**: Delete all kubernetes connections. 25 | `, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | all, err := cmd.Flags().GetBool("all") 28 | if err != nil { 29 | color.Red("Error parsing flags: %v", err) 30 | return 31 | } 32 | 33 | if all { 34 | err := ui.RunWithSpinner("Deleting all kubernetes connections...", deleteAllKubernetesConnections) 35 | if err != nil { 36 | color.Red("Failed to delete all kubernetes connections: %v", err) 37 | } 38 | return 39 | } else if len(args) == 1 { 40 | connectionName := args[0] 41 | err := ui.RunWithSpinner(fmt.Sprintf("Deleting kubernetes connection '%s'...", connectionName), func() error { 42 | return deleteKubernetesConnection(connectionName) 43 | }) 44 | if err != nil { 45 | color.Red("Failed to delete kubernetes connection '%s': %v", connectionName, err) 46 | } 47 | return 48 | } else { 49 | selectedConnection, err := runInteractiveDelete() 50 | if err != nil { 51 | color.Red("Error: %v", err) 52 | return 53 | } 54 | if selectedConnection != "" { 55 | err := ui.RunWithSpinner(fmt.Sprintf("Deleting kubernetes connection '%s'...", selectedConnection), func() error { 56 | return deleteKubernetesConnection(selectedConnection) 57 | }) 58 | if err != nil { 59 | color.Red("Failed to delete kubernetes connection '%s': %v", selectedConnection, err) 60 | } 61 | } 62 | } 63 | }, 64 | } 65 | 66 | deleteCmd.Flags().Bool("all", false, "Delete all kubernetes connections") 67 | 68 | return deleteCmd 69 | } 70 | 71 | // deleteAllKubernetesConnections deletes all kubernetes connections 72 | func deleteAllKubernetesConnections() error { 73 | if err := config.DeleteAllConnectionsByType(conn.ConnectionTypeCloud); err != nil { 74 | return fmt.Errorf("error deleting all kubernetes connections: %w", err) 75 | } 76 | return nil 77 | } 78 | 79 | // deleteKubernetesConnection deletes a single kubernetes connection by name 80 | func deleteKubernetesConnection(name string) error { 81 | if err := config.DeleteConnectionByName(name); err != nil { 82 | return fmt.Errorf("error deleting kubernetes connection: %w", err) 83 | } 84 | return nil 85 | } 86 | 87 | // runInteractiveDelete runs the Bubble Tea program for interactive deletion 88 | func runInteractiveDelete() (string, error) { 89 | connections, err := config.GetConnectionsByType(conn.ConnectionTypeCloud) 90 | if err != nil { 91 | return "", fmt.Errorf("getting connections: %w", err) 92 | } 93 | 94 | items := make([]table.Row, len(connections)) 95 | for i, conn := range connections { 96 | items[i] = table.Row{conn.Name, conn.Type.GetMainType(), conn.Type.GetSubtype()} 97 | } 98 | 99 | columns := []table.Column{ 100 | {Title: "Name", Width: 25}, 101 | {Title: "Type", Width: 15}, 102 | {Title: "Driver", Width: 20}, 103 | } 104 | 105 | t := table.New( 106 | table.WithColumns(columns), 107 | table.WithRows(items), 108 | table.WithFocused(true), 109 | table.WithHeight(10), 110 | ) 111 | 112 | s := table.DefaultStyles() 113 | s.Header = s.Header. 114 | BorderStyle(lipgloss.NormalBorder()). 115 | BorderForeground(lipgloss.Color("240")). 116 | BorderBottom(true). 117 | Bold(false) 118 | s.Selected = s.Selected. 119 | Foreground(lipgloss.Color("0")). 120 | Background(lipgloss.Color("212")). 121 | Bold(true) 122 | t.SetStyles(s) 123 | 124 | deleteTableModel := ui.NewTableModel(t, nil, false) 125 | 126 | p := tea.NewProgram(deleteTableModel) 127 | if _, err := p.Run(); err != nil { 128 | return "", fmt.Errorf("running Bubble Tea program: %w", err) 129 | } 130 | 131 | return deleteTableModel.Selected(), nil 132 | } 133 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/k8s/kubernetes.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func NewRootCommand() *cobra.Command { 8 | cmd := &cobra.Command{ 9 | Use: "kubernetes", 10 | Aliases: []string{"k8s"}, 11 | Short: "Manage kubernetes connections.", 12 | Long: ` 13 | Kubernetes Connection: 14 | 15 | - Available Kubernetes connection types: All Kubernetes clusters defined in your configuration. 16 | - Commands: create, delete, open, list, types. 17 | - Examples: 18 | * 'pops conn k8s create' creates a connection to a Kubernetes cluster. 19 | * 'pops conn k8s open' opens an existing Kubernetes connection. 20 | * 'pops conn k8s list' lists all Kubernetes connections. 21 | * 'pops conn k8s delete' deletes a Kubernetes connection. 22 | * 'pops conn k8s types' lists all available Kubernetes connection types (for now; all Kubernetes clusters defined in your configuration). 23 | 24 | More connection types and features are coming soon!`, 25 | } 26 | 27 | // `pops connection kubernetes create *` commands 28 | cmd.AddCommand(newCreateCmd()) 29 | 30 | // `pops connection kubernetes open *` commands 31 | cmd.AddCommand(newOpenCmd()) 32 | 33 | // `pops connection kubernetes list` command 34 | cmd.AddCommand(newListCmd()) 35 | 36 | // `pops connection kubernetes delete *` commands 37 | cmd.AddCommand(newDeleteCmd()) 38 | 39 | // `pops connection kubernetes types` command 40 | cmd.AddCommand(newTypesCmd()) 41 | 42 | return cmd 43 | } 44 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/k8s/list.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | config "github.com/prompt-ops/pops/pkg/config" 8 | "github.com/prompt-ops/pops/pkg/conn" 9 | "github.com/prompt-ops/pops/pkg/ui" 10 | 11 | "github.com/charmbracelet/bubbles/table" 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/charmbracelet/lipgloss" 14 | "github.com/fatih/color" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | func newListCmd() *cobra.Command { 19 | listCmd := &cobra.Command{ 20 | Use: "list", 21 | Short: "List all kubernetes connections", 22 | Long: "List all kubernetes connections that have been set up.", 23 | Run: func(cmd *cobra.Command, args []string) { 24 | if err := runListConnections(); err != nil { 25 | color.Red("Error listing kubernetes connections: %v", err) 26 | os.Exit(1) 27 | } 28 | }, 29 | } 30 | 31 | return listCmd 32 | } 33 | 34 | // runListConnections lists all connections 35 | func runListConnections() error { 36 | connections, err := config.GetConnectionsByType(conn.ConnectionTypeKubernetes) 37 | if err != nil { 38 | return fmt.Errorf("getting kubernetes connections: %w", err) 39 | } 40 | 41 | items := make([]table.Row, len(connections)) 42 | for i, conn := range connections { 43 | items[i] = table.Row{conn.Name, conn.Type.GetMainType(), conn.Type.GetSubtype()} 44 | } 45 | 46 | columns := []table.Column{ 47 | {Title: "Name", Width: 25}, 48 | {Title: "Type", Width: 15}, 49 | {Title: "Subtype", Width: 20}, 50 | } 51 | 52 | t := table.New( 53 | table.WithColumns(columns), 54 | table.WithRows(items), 55 | table.WithFocused(true), 56 | table.WithHeight(10), 57 | ) 58 | 59 | s := table.DefaultStyles() 60 | s.Header = s.Header. 61 | BorderStyle(lipgloss.NormalBorder()). 62 | BorderForeground(lipgloss.Color("240")). 63 | BorderBottom(true). 64 | Bold(false) 65 | s.Selected = s.Selected. 66 | Foreground(lipgloss.Color("0")). 67 | Background(lipgloss.Color("212")). 68 | Bold(true) 69 | t.SetStyles(s) 70 | 71 | openTableModel := ui.NewTableModel(t, nil, true) 72 | 73 | p := tea.NewProgram(openTableModel) 74 | if _, err := p.Run(); err != nil { 75 | panic(err) 76 | } 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/k8s/open.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "github.com/prompt-ops/pops/pkg/ui" 5 | k8sui "github.com/prompt-ops/pops/pkg/ui/conn/k8s" 6 | "github.com/prompt-ops/pops/pkg/ui/shell" 7 | 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type openModel struct { 13 | current tea.Model 14 | } 15 | 16 | func initialOpenModel() *openModel { 17 | return &openModel{ 18 | current: k8sui.NewOpenModel(), 19 | } 20 | } 21 | 22 | // NewOpenModel returns a new openModel 23 | func NewOpenModel() *openModel { 24 | return initialOpenModel() 25 | } 26 | 27 | func (m *openModel) Init() tea.Cmd { 28 | return m.current.Init() 29 | } 30 | 31 | func (m *openModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 32 | switch msg := msg.(type) { 33 | case ui.TransitionToShellMsg: 34 | shell := shell.NewShellModel(msg.Connection) 35 | return shell, shell.Init() 36 | } 37 | var cmd tea.Cmd 38 | m.current, cmd = m.current.Update(msg) 39 | return m, cmd 40 | } 41 | 42 | func (m *openModel) View() string { 43 | return m.current.View() 44 | } 45 | 46 | func newOpenCmd() *cobra.Command { 47 | cmd := &cobra.Command{ 48 | Use: "open", 49 | Short: "Create a new Kubernetes connection.", 50 | Run: func(cmd *cobra.Command, args []string) { 51 | p := tea.NewProgram(initialOpenModel()) 52 | if _, err := p.Run(); err != nil { 53 | panic(err) 54 | } 55 | }, 56 | } 57 | return cmd 58 | } 59 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/k8s/types.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/prompt-ops/pops/pkg/conn" 7 | "github.com/prompt-ops/pops/pkg/ui" 8 | 9 | "github.com/charmbracelet/bubbles/table" 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | "github.com/fatih/color" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func newTypesCmd() *cobra.Command { 17 | listCmd := &cobra.Command{ 18 | Use: "types", 19 | Short: "List all available kubernetes connection types", 20 | Long: "List all available kubernetes connection types", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | if err := runListAvaibleKubernetesTypes(); err != nil { 23 | color.Red("Error listing kubernetes connections: %v", err) 24 | os.Exit(1) 25 | } 26 | }, 27 | } 28 | 29 | return listCmd 30 | } 31 | 32 | // runListAvaibleKubernetesTypes lists all available kubernetes connection types 33 | func runListAvaibleKubernetesTypes() error { 34 | connectionTypes := conn.AvailableKubernetesConnectionTypes 35 | 36 | items := make([]table.Row, len(connectionTypes)) 37 | for i, connectionType := range connectionTypes { 38 | items[i] = table.Row{connectionType.Subtype} 39 | } 40 | 41 | columns := []table.Column{ 42 | {Title: "Available Types", Width: 25}, 43 | } 44 | 45 | t := table.New( 46 | table.WithColumns(columns), 47 | table.WithRows(items), 48 | table.WithFocused(true), 49 | table.WithHeight(10), 50 | ) 51 | 52 | s := table.DefaultStyles() 53 | s.Header = s.Header. 54 | BorderStyle(lipgloss.NormalBorder()). 55 | BorderForeground(lipgloss.Color("240")). 56 | BorderBottom(true). 57 | Bold(false) 58 | s.Selected = s.Selected. 59 | Foreground(lipgloss.Color("0")). 60 | Background(lipgloss.Color("212")). 61 | Bold(true) 62 | t.SetStyles(s) 63 | 64 | openTableModel := ui.NewTableModel(t, nil, true) 65 | 66 | p := tea.NewProgram(openTableModel) 67 | if _, err := p.Run(); err != nil { 68 | panic(err) 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/list.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | config "github.com/prompt-ops/pops/pkg/config" 8 | "github.com/prompt-ops/pops/pkg/ui" 9 | 10 | "github.com/charmbracelet/bubbles/table" 11 | tea "github.com/charmbracelet/bubbletea" 12 | "github.com/charmbracelet/lipgloss" 13 | "github.com/fatih/color" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func newListCmd() *cobra.Command { 18 | listCmd := &cobra.Command{ 19 | Use: "list", 20 | Short: "List all connections", 21 | Long: "List all connections that have been set up.", 22 | Run: func(cmd *cobra.Command, args []string) { 23 | if err := runListConnections(); err != nil { 24 | color.Red("Error listing connections: %v", err) 25 | os.Exit(1) 26 | } 27 | }, 28 | } 29 | 30 | return listCmd 31 | } 32 | 33 | // runListConnections lists all connections 34 | func runListConnections() error { 35 | connections, err := config.GetAllConnections() 36 | if err != nil { 37 | return fmt.Errorf("getting connections: %w", err) 38 | } 39 | 40 | items := make([]table.Row, len(connections)) 41 | for i, conn := range connections { 42 | items[i] = table.Row{conn.Name, conn.Type.GetMainType(), conn.Type.GetSubtype()} 43 | } 44 | 45 | columns := []table.Column{ 46 | {Title: "Name", Width: 25}, 47 | {Title: "Type", Width: 15}, 48 | {Title: "Subtype", Width: 20}, 49 | } 50 | 51 | t := table.New( 52 | table.WithColumns(columns), 53 | table.WithRows(items), 54 | table.WithFocused(true), 55 | table.WithHeight(10), 56 | ) 57 | 58 | s := table.DefaultStyles() 59 | s.Header = s.Header. 60 | BorderStyle(lipgloss.NormalBorder()). 61 | BorderForeground(lipgloss.Color("240")). 62 | BorderBottom(true). 63 | Bold(false) 64 | s.Selected = s.Selected. 65 | Foreground(lipgloss.Color("0")). 66 | Background(lipgloss.Color("212")). 67 | Bold(true) 68 | t.SetStyles(s) 69 | 70 | openTableModel := ui.NewTableModel(t, nil, true) 71 | 72 | p := tea.NewProgram(openTableModel) 73 | if _, err := p.Run(); err != nil { 74 | panic(err) 75 | } 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/open.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "github.com/prompt-ops/pops/pkg/config" 5 | "github.com/prompt-ops/pops/pkg/ui/conn" 6 | "github.com/prompt-ops/pops/pkg/ui/shell" 7 | 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/fatih/color" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // newOpenCmd creates the open command for the connection. 14 | func newOpenCmd() *cobra.Command { 15 | openCmd := &cobra.Command{ 16 | Use: "open", 17 | Short: "Open a connection", 18 | Long: "Open a connection", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | if len(args) == 1 { 21 | openSingleConnection(args[0]) 22 | } else { 23 | openConnectionPicker() 24 | } 25 | }, 26 | } 27 | 28 | return openCmd 29 | } 30 | 31 | // openSingleConnection opens a single connection by name. 32 | func openSingleConnection(name string) { 33 | conn, err := config.GetConnectionByName(name) 34 | if err != nil { 35 | color.Red("Error getting connection: %v", err) 36 | return 37 | } 38 | 39 | shell := shell.NewShellModel(conn) 40 | p := tea.NewProgram(shell) 41 | if _, err := p.Run(); err != nil { 42 | color.Red("Error opening shell UI: %v", err) 43 | } 44 | } 45 | 46 | // openConnectionPicker opens the connection picker UI. 47 | func openConnectionPicker() { 48 | root := conn.NewOpenRootModel() 49 | p := tea.NewProgram(root) 50 | if _, err := p.Run(); err != nil { 51 | color.Red("Error: %v", err) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cmd/pops/app/conn/types.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/prompt-ops/pops/pkg/conn" 7 | "github.com/prompt-ops/pops/pkg/ui" 8 | 9 | "github.com/charmbracelet/bubbles/table" 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | "github.com/fatih/color" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func newTypesCmd() *cobra.Command { 17 | listCmd := &cobra.Command{ 18 | Use: "types", 19 | Short: "List all available connection types", 20 | Long: "List all available connection types", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | if err := runListAvaibleTypes(); err != nil { 23 | color.Red("Error listing connections: %v", err) 24 | os.Exit(1) 25 | } 26 | }, 27 | } 28 | 29 | return listCmd 30 | } 31 | 32 | // runListAvaibleTypes lists all available connection types 33 | func runListAvaibleTypes() error { 34 | connectionTypes := conn.AvailableConnectionTypes() 35 | 36 | items := make([]table.Row, len(connectionTypes)) 37 | for i, connectionType := range connectionTypes { 38 | items[i] = table.Row{ 39 | connectionType, 40 | } 41 | } 42 | 43 | columns := []table.Column{ 44 | {Title: "Available Types", Width: 25}, 45 | } 46 | 47 | t := table.New( 48 | table.WithColumns(columns), 49 | table.WithRows(items), 50 | table.WithFocused(true), 51 | table.WithHeight(10), 52 | ) 53 | 54 | s := table.DefaultStyles() 55 | s.Header = s.Header. 56 | BorderStyle(lipgloss.NormalBorder()). 57 | BorderForeground(lipgloss.Color("240")). 58 | BorderBottom(true). 59 | Bold(false) 60 | s.Selected = s.Selected. 61 | Foreground(lipgloss.Color("0")). 62 | Background(lipgloss.Color("212")). 63 | Bold(true) 64 | t.SetStyles(s) 65 | 66 | openTableModel := ui.NewTableModel(t, nil, true) 67 | 68 | p := tea.NewProgram(openTableModel) 69 | if _, err := p.Run(); err != nil { 70 | panic(err) 71 | } 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /cmd/pops/app/root.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/prompt-ops/pops/cmd/pops/app/conn" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewRootCommand() *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "pops", 14 | Short: "Prompt-Ops manages your infrastructure using natural language.", 15 | } 16 | 17 | // `pops version` command 18 | cmd.AddCommand(NewVersionCmd) 19 | 20 | // `pops connection (conn as alias)` commands 21 | cmd.AddCommand(conn.NewConnectionCommand()) 22 | 23 | return cmd 24 | } 25 | 26 | // Execute adds all child commands to the root command and sets flags appropriately. 27 | func Execute() { 28 | rootCmd := NewRootCommand() 29 | if err := rootCmd.Execute(); err != nil { 30 | fmt.Fprintf(os.Stdout, "Error: %s\n", err) 31 | os.Exit(1) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cmd/pops/app/version.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var version = "dev" 10 | 11 | var NewVersionCmd = &cobra.Command{ 12 | Use: "version", 13 | Short: "Print the version number of Prompt-Ops", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | fmt.Printf("Prompt-Ops version %s\n", version) 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /cmd/pops/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/prompt-ops/pops/cmd/pops/app" 7 | ) 8 | 9 | func main() { 10 | err := app.NewRootCommand().Execute() 11 | if err != nil { 12 | os.Exit(1) //nolint:forbidigo // this is OK inside the main function. 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/contributing/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Prompt-Ops 2 | 3 | ## Table of Contents 4 | 5 | - [Adding a new connection type](connection/ADD_TYPE.md). 6 | - [Adding a subtype to a connection](connection/ADD_SUBTYPE.md). 7 | - [Adding a new AI model](ai/ADD_AI_MODEL.md). 8 | -------------------------------------------------------------------------------- /docs/contributing/ai/ADD_AI_MODEL.md: -------------------------------------------------------------------------------- 1 | # Add a new AI Model 2 | 3 | ## Introduction 4 | 5 | Prompt-Ops has connection types and connection subtypes. Some examples are as follows: 6 | 7 | | Connection Type | Connection Subtype | 8 | | --------------- | --------------------------------------------------------- | 9 | | Database | PostgreSQL | 10 | | Database | MySQL | 11 | | Database | MongoDB | 12 | | Cloud Provider | Azure | 13 | | Cloud Provider | AWS | 14 | | Kubernetes | (No subtype - any cluster selected during initialization) | 15 | 16 | Prompt-Ops also has access to AI models that help power this application. 17 | 18 | | Creator | Model | 19 | | ------- | ------ | 20 | | OpenAI | gpt-4o | 21 | 22 | ## How to add a new AI model 23 | 24 | 1. Create a new file under `pkg/ai` for the new AI model. For example, `meta.go` for Meta being the creator of `Llama 3.1`. 25 | 2. Implement the `AIModel` interface in `pkg/ai/types.go` for the new AI model. Define the functions needed by the interface. 26 | 3. For an example, please see `OpenAIModel` in `pkg/ai/openai.go`. 27 | 4. Suggest improvements if you think the code structure can be enhanced. We welcome new ideas. 28 | 5. Naming may not sound right but as time passes we are going to improve. 29 | -------------------------------------------------------------------------------- /docs/contributing/connection/ADD_SUBTYPE.md: -------------------------------------------------------------------------------- 1 | # Add a new Connection Subtype 2 | 3 | ## Introduction 4 | 5 | Prompt-Ops has connection types and connection subtypes. Some examples are as follows: 6 | 7 | | Connection Type | Connection Subtype | 8 | | --------------- | --------------------------------------------------------- | 9 | | Database | PostgreSQL | 10 | | Database | MySQL | 11 | | Database | MongoDB | 12 | | Cloud Provider | Azure | 13 | | Cloud Provider | AWS | 14 | | Kubernetes | (No subtype - any cluster selected during initialization) | 15 | 16 | ## How to add a new connection subtype 17 | 18 | 1. Implement the `ConnectionInterface` in `pkg/connection/types.go` under the proper connection type. 19 | 2. For an example, please see `PostgreSQLConnection` in `pkg/connection/db.go`. 20 | 3. Suggest improvements if you think the code structure can be enhanced. We welcome new ideas. 21 | 4. Consider creating new files for new subtypes to keep the main connection file manageable. 22 | -------------------------------------------------------------------------------- /docs/contributing/connection/ADD_TYPE.md: -------------------------------------------------------------------------------- 1 | # Add a new Connection Type 2 | 3 | ## Introduction 4 | 5 | Prompt-Ops has connection types and connection subtypes. Some examples are as follows: 6 | 7 | | Connection Type | Connection Subtype | 8 | | --------------- | --------------------------------------------------------- | 9 | | Database | PostgreSQL | 10 | | Database | MySQL | 11 | | Database | MongoDB | 12 | | Cloud Provider | Azure | 13 | | Cloud Provider | AWS | 14 | | Kubernetes | (No subtype - any cluster selected during initialization) | 15 | 16 | ## How to add a new connection type 17 | 18 | 1. Create a new file under `pkg/connection` for the new connection type. For example, `mqs.go` for message queues. 19 | 2. Define the new connection type in `pkg/connection/mqs.go`. 20 | 3. Implement the `ConnectionInterface` for the new connection type. 21 | 4. For an example, please see `DatabaseConnectionType` in `pkg/connection/db.go`. 22 | 5. Now you can create subtypes for the new connection. 23 | 6. Suggest improvements if you think the code structure can be enhanced. We welcome new ideas. 24 | 7. Consider creating new files for new connection types to keep the main connection file manageable. 25 | -------------------------------------------------------------------------------- /docs/examples/README.md: -------------------------------------------------------------------------------- 1 | # Prompt-Ops Examples 2 | 3 | ## Connection Examples 4 | 5 | ### Kubernetes 6 | 7 | - [`pops conn k8s create` example](conn/k8s/README.md) 8 | -------------------------------------------------------------------------------- /docs/examples/conn/k8s/README.md: -------------------------------------------------------------------------------- 1 | # Prompt-Ops Kubernetes Connection Examples 2 | 3 | ## Create a new Connection 4 | 5 | To create a new Kubernetes connection, use the following command: 6 | 7 | ```bash 8 | pops conn k8s create 9 | ``` 10 | 11 | Demo: 12 | 13 | ![pops conn k8s create](./pops_conn_k8s_create_v1.gif) 14 | 15 | ## Open an existing Connection 16 | 17 | ## Ask a Question via Prompt-Ops 18 | 19 | ## Request a Command via Prompt-Ops 20 | -------------------------------------------------------------------------------- /docs/examples/conn/k8s/pops_conn_k8s_create_v1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-ops/pops/385ae118d6e440c0ad9125ce5b33f5ad488d20bb/docs/examples/conn/k8s/pops_conn_k8s_create_v1.gif -------------------------------------------------------------------------------- /docs/examples/conn/k8s/pops_conn_k8s_create_v1.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-ops/pops/385ae118d6e440c0ad9125ce5b33f5ad488d20bb/docs/examples/conn/k8s/pops_conn_k8s_create_v1.mov -------------------------------------------------------------------------------- /docs/releases/RELEASE_PROCESS.md: -------------------------------------------------------------------------------- 1 | # Prompt-Ops Release Process 2 | 3 | ## Introduction 4 | 5 | TBD 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prompt-ops/pops 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.20.0 7 | github.com/charmbracelet/bubbletea v1.2.4 8 | github.com/charmbracelet/lipgloss v1.0.0 9 | github.com/fatih/color v1.18.0 10 | github.com/lib/pq v1.10.9 11 | github.com/olekukonko/tablewriter v0.0.5 12 | github.com/openai/openai-go v0.1.0-alpha.49 13 | github.com/spf13/cobra v1.8.1 14 | github.com/stretchr/testify v1.10.0 15 | golang.org/x/term v0.28.0 16 | ) 17 | 18 | require ( 19 | github.com/atotto/clipboard v0.1.4 // indirect 20 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 21 | github.com/charmbracelet/x/ansi v0.7.0 // indirect 22 | github.com/charmbracelet/x/term v0.2.1 // indirect 23 | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 26 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 27 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 28 | github.com/mattn/go-colorable v0.1.14 // indirect 29 | github.com/mattn/go-isatty v0.0.20 // indirect 30 | github.com/mattn/go-localereader v0.0.1 // indirect 31 | github.com/mattn/go-runewidth v0.0.16 // indirect 32 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 33 | github.com/muesli/cancelreader v0.2.2 // indirect 34 | github.com/muesli/termenv v0.15.2 // indirect 35 | github.com/pmezard/go-difflib v1.0.0 // indirect 36 | github.com/rivo/uniseg v0.4.7 // indirect 37 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 38 | github.com/spf13/pflag v1.0.5 // indirect 39 | github.com/tidwall/gjson v1.18.0 // indirect 40 | github.com/tidwall/match v1.1.1 // indirect 41 | github.com/tidwall/pretty v1.2.1 // indirect 42 | github.com/tidwall/sjson v1.2.5 // indirect 43 | golang.org/x/sync v0.10.0 // indirect 44 | golang.org/x/sys v0.29.0 // indirect 45 | golang.org/x/text v0.21.0 // indirect 46 | gopkg.in/yaml.v3 v3.0.1 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 6 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 7 | github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= 8 | github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= 9 | github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= 10 | github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= 11 | github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= 12 | github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= 13 | github.com/charmbracelet/x/ansi v0.7.0 h1:/QfFmiXOGGwN6fRbzvQaYp7fu1pkxpZ3qFBZWBsP404= 14 | github.com/charmbracelet/x/ansi v0.7.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= 15 | github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= 16 | github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 17 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 18 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 19 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 20 | github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= 21 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 25 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 26 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 27 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 28 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 29 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 30 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 31 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 32 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 33 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 34 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 35 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 36 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 37 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 38 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 39 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 40 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 41 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 42 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 43 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 44 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 45 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 46 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 47 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 48 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 49 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 50 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 51 | github.com/openai/openai-go v0.1.0-alpha.49 h1:58Wnz1ElmAShgJ9jJQq4XDVzCAuTjK9Yi4cciA9TICM= 52 | github.com/openai/openai-go v0.1.0-alpha.49/go.mod h1:3SdE6BffOX9HPEQv8IL/fi3LYZ5TUpRYaqGQZbyk11A= 53 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 54 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 55 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 56 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 57 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 58 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 59 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 60 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 61 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 62 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 63 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 64 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 65 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 66 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 67 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 68 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 69 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 70 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 71 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 72 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 73 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 74 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 75 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 76 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 77 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 78 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 81 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 82 | golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= 83 | golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 84 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 85 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 87 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 88 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 89 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | -------------------------------------------------------------------------------- /make/build.mk: -------------------------------------------------------------------------------- 1 | GOOS ?= $(shell go env GOOS) 2 | GOARCH ?= $(shell go env GOARCH) 3 | GOPATH := $(shell go env GOPATH) 4 | 5 | GO111MODULE ?= on 6 | CGO_ENABLED ?= 0 7 | VERSION ?= dev 8 | 9 | .PHONY: build 10 | build: 11 | @echo "Building Prompt-Ops..." 12 | @echo "GOOS: $(GOOS)" 13 | @echo "GOARCH: $(GOARCH)" 14 | @echo "GOPATH: $(GOPATH)" 15 | @echo "GO111MODULE: $(GO111MODULE)" 16 | @echo "CGO_ENABLED: $(CGO_ENABLED)" 17 | @echo "VERSION: $(VERSION)" 18 | @go build -ldflags="-s -w -X github.com/prompt-ops/pops/cmd/pops/app.version=$(VERSION)" -o dist/pops-$(GOOS)-$(GOARCH) cmd/pops/main.go 19 | @echo "Build complete." -------------------------------------------------------------------------------- /make/format.mk: -------------------------------------------------------------------------------- 1 | .PHONY: organize-imports 2 | 3 | organize-imports: 4 | @echo "Organizing imports and formatting code with goimports..." 5 | @goimports -w . 6 | @echo "Formatting complete." -------------------------------------------------------------------------------- /make/gendocs.mk: -------------------------------------------------------------------------------- 1 | OUTPUT_PATH ?= docs 2 | 3 | .PHONY: generate-cli-docs 4 | generate-cli-docs: 5 | @echo "Generating CLI docs for Prompt-Ops..." 6 | @go run cmd/docgen/main.go $(OUTPUT_PATH) 7 | @echo "Generation complete. Docs generated at $(OUTPUT_PATH)" -------------------------------------------------------------------------------- /make/install.mk: -------------------------------------------------------------------------------- 1 | GOOS ?= $(shell go env GOOS) 2 | GOARCH ?= $(shell go env GOARCH) 3 | GOPATH := $(shell go env GOPATH) 4 | 5 | VERSION ?= dev 6 | 7 | .PHONY: install 8 | install: build 9 | @echo "Installing Prompt-Ops version $(VERSION)..." 10 | 11 | @echo "Deleting existing installation..." 12 | @rm -f /usr/local/bin/pops 13 | 14 | @echo "Copying new installation..." 15 | @cp dist/pops-$(GOOS)-$(GOARCH) /usr/local/bin/pops 16 | 17 | @echo "Installation complete." -------------------------------------------------------------------------------- /make/lint.mk: -------------------------------------------------------------------------------- 1 | GOOS ?= $(shell go env GOOS) 2 | 3 | ifeq ($(GOOS),windows) 4 | GOLANGCI_LINT:=golangci-lint.exe 5 | else 6 | GOLANGCI_LINT:=golangci-lint 7 | endif 8 | 9 | .PHONY: lint 10 | lint: 11 | @echo "Running Go linter..." 12 | @$(GOLANGCI_LINT) run --fix --timeout 5m 13 | @echo "Linting complete." -------------------------------------------------------------------------------- /make/test.mk: -------------------------------------------------------------------------------- 1 | .PHONY: unit-test 2 | 3 | unit-test: 4 | @echo "Running unit tests..." 5 | @go test ./... -v -------------------------------------------------------------------------------- /pkg/ai/openai.go: -------------------------------------------------------------------------------- 1 | package ai 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/openai/openai-go" 12 | "github.com/openai/openai-go/option" 13 | "github.com/openai/openai-go/shared" 14 | ) 15 | 16 | var ( 17 | // defaultSystemMessage is the system message that is sent to the OpenAI API to help it 18 | // understand the context of the user's input. 19 | defaultSystemMessage = ` 20 | You are a helpful assistant that translates natural language commands to %s. 21 | You must output your response without any code fences or triple backticks. 22 | If you have SQL code or other commands, simply put them after the word “Command:” with no markdown. 23 | For example: 24 | 25 | Command: az vm list 26 | Suggested next steps: 27 | 1. Start a specific VM. 28 | 2. Stop a specific VM. 29 | 30 | Command: kubectl get pods 31 | Suggested next steps: 32 | 1. Describe one of the pods. 33 | 2. Delete a specific pod. 34 | 35 | Command: aws ec2 describe-instances 36 | Suggested next steps: 37 | 1. Start a specific instance. 38 | 2. Stop a specific instance. 39 | 40 | Command: SELECT * FROM table_name; 41 | Suggested next steps: 42 | 1. Filter the results based on a specific condition. 43 | 2. Join this table with another table. 44 | 45 | No triple backticks. No code fences. Plain text only. 46 | Do not include any Markdown formatting (like triple backticks or bullet points other than the step numbering). 47 | Do not include any additional explanation or text besides what is requested. 48 | No triple backticks. No code fences. Plain text only. 49 | ` 50 | ) 51 | 52 | // OpenAIModel is the OpenAI implementation of the AIModel interface. 53 | type OpenAIModel struct { 54 | apiKey string 55 | client *openai.Client 56 | chatModel openai.ChatModel 57 | commandType string 58 | context string 59 | } 60 | 61 | func NewOpenAIModel(commandType, context string) (*OpenAIModel, error) { 62 | apiKey := os.Getenv("OPENAI_API_KEY") 63 | if apiKey == "" { 64 | return nil, fmt.Errorf("OpenAI API key not set") 65 | } 66 | 67 | client := openai.NewClient( 68 | option.WithAPIKey(apiKey), 69 | ) 70 | 71 | return &OpenAIModel{ 72 | apiKey: apiKey, 73 | client: client, 74 | chatModel: openai.ChatModelGPT4o, // example model 75 | commandType: commandType, 76 | context: context, 77 | }, nil 78 | } 79 | 80 | func (o *OpenAIModel) GetName() string { 81 | return "OpenAI" 82 | } 83 | 84 | func (o *OpenAIModel) GetAPIKey() string { 85 | return o.apiKey 86 | } 87 | 88 | func (o *OpenAIModel) SetChatModel(chatModel openai.ChatModel) { 89 | o.chatModel = chatModel 90 | } 91 | 92 | func (o *OpenAIModel) GetChatModel() openai.ChatModel { 93 | return o.chatModel 94 | } 95 | 96 | func (o *OpenAIModel) SetCommandType(commandType string) { 97 | o.commandType = commandType 98 | } 99 | 100 | func (o *OpenAIModel) GetCommandType() string { 101 | return o.commandType 102 | } 103 | 104 | func (o *OpenAIModel) SetContext(context string) { 105 | o.context = context 106 | } 107 | 108 | func (o *OpenAIModel) GetContext() string { 109 | return o.context 110 | } 111 | 112 | // GetCommand calls the OpenAI API with tool calling (if supported), then falls back to text parsing if no tool call is made. 113 | func (o *OpenAIModel) GetCommand(prompt string) (*AIResponse, error) { 114 | // 1) Define the tool(s) the model may call. 115 | tools := []openai.ChatCompletionToolParam{ 116 | { 117 | Function: openai.F(shared.FunctionDefinitionParam{ 118 | Name: openai.F("generateCommand"), 119 | Description: openai.F("Generate a command and suggested next steps."), 120 | Parameters: openai.F(shared.FunctionParameters{ 121 | "type": "object", 122 | "properties": map[string]interface{}{ 123 | "command": map[string]interface{}{ 124 | "type": "string", 125 | "description": "The command to run, e.g. 'az vm list'", 126 | }, 127 | "suggestedNextSteps": map[string]interface{}{ 128 | "type": "array", 129 | "items": map[string]interface{}{"type": "string"}, 130 | "description": "A list of suggested next steps such as '1. Start a specific VM.' etc.", 131 | }, 132 | }, 133 | "required": []string{"command", "suggestedNextSteps"}, 134 | "additionalProperties": false, 135 | }), 136 | Strict: openai.F(true), 137 | }), 138 | Type: openai.F(openai.ChatCompletionToolTypeFunction), 139 | }, 140 | } 141 | 142 | // 2) Create the chat completion request with tool definitions. Depending on your client, you might use a different way to specify tool calling. 143 | chatCompletion, err := o.client.Chat.Completions.New(context.TODO(), openai.ChatCompletionNewParams{ 144 | Messages: openai.F([]openai.ChatCompletionMessageParamUnion{ 145 | openai.SystemMessage(fmt.Sprintf(defaultSystemMessage, o.GetCommandType())), 146 | openai.SystemMessage(o.GetContext()), 147 | openai.UserMessage(prompt), 148 | }), 149 | Model: openai.F(o.GetChatModel()), 150 | ToolChoice: openai.F[openai.ChatCompletionToolChoiceOptionUnionParam](openai.ChatCompletionToolChoiceOptionAutoRequired), 151 | Tools: openai.F(tools), 152 | Temperature: openai.F(0.2), 153 | }) 154 | if err != nil { 155 | return nil, fmt.Errorf("error from OpenAI API: %v", err) 156 | } 157 | 158 | // 3) Check the response. The model might return a direct text answer or a tool call. 159 | if len(chatCompletion.Choices) == 0 { 160 | return nil, fmt.Errorf("no choices returned from OpenAI") 161 | } 162 | 163 | choice := chatCompletion.Choices[0] 164 | 165 | // If the model returned a tool call, parse the JSON in toolCall.Arguments. 166 | if choice.Message.ToolCalls != nil { 167 | return parseToolCalls(choice.Message.ToolCalls) 168 | } 169 | 170 | // Otherwise, fallback to your existing text-based parsing. 171 | response := strings.TrimSpace(choice.Message.Content) 172 | responseStr := stripMarkdownFences(response) 173 | parsedAIResponse, err := parseResponse(responseStr) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | return &parsedAIResponse, nil 179 | } 180 | 181 | func (o *OpenAIModel) GetAnswer(prompt string) (*AIResponse, error) { 182 | chatCompletion, err := o.client.Chat.Completions.New(context.TODO(), openai.ChatCompletionNewParams{ 183 | Messages: openai.F([]openai.ChatCompletionMessageParamUnion{ 184 | openai.SystemMessage(o.GetContext()), 185 | openai.UserMessage(prompt), 186 | }), 187 | Model: openai.F(o.GetChatModel()), 188 | Temperature: openai.F(0.2), 189 | }) 190 | if err != nil { 191 | return nil, fmt.Errorf("error from OpenAI API: %v", err) 192 | } 193 | 194 | if len(chatCompletion.Choices) == 0 { 195 | return nil, fmt.Errorf("no choices returned from OpenAI") 196 | } 197 | 198 | choice := chatCompletion.Choices[0] 199 | 200 | response := strings.TrimSpace(choice.Message.Content) 201 | responseStr := stripMarkdownFences(response) 202 | 203 | return &AIResponse{ 204 | Prompt: prompt, 205 | Answer: responseStr, 206 | }, nil 207 | } 208 | 209 | // parseToolCall unmarshals the model's tool call arguments into AIResponse. 210 | func parseToolCalls(toolCalls []openai.ChatCompletionMessageToolCall) (*AIResponse, error) { 211 | if len(toolCalls) == 0 { 212 | return nil, fmt.Errorf("no tool calls found") 213 | } 214 | 215 | for _, toolCall := range toolCalls { 216 | if toolCall.Function.Name == "generateCommand" { 217 | var args struct { 218 | Command string `json:"command"` 219 | SuggestedNextSteps []string `json:"suggestedNextSteps"` 220 | } 221 | if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil { 222 | return nil, fmt.Errorf("failed to unmarshal tool call args: %v", err) 223 | } 224 | 225 | return &AIResponse{ 226 | Command: args.Command, 227 | NextSteps: args.SuggestedNextSteps, 228 | }, nil 229 | } 230 | } 231 | 232 | return nil, fmt.Errorf("needed tool call could not be found in the tool calls") 233 | } 234 | 235 | // parseResponse processes the AI response to extract the command and suggested next steps 236 | // when the model doesn't perform a tool calling. 237 | func parseResponse(response string) (AIResponse, error) { 238 | parsed := AIResponse{} 239 | 240 | // Split the response into lines for parsing 241 | lines := strings.Split(response, "\n") 242 | 243 | for i := 0; i < len(lines); i++ { 244 | line := strings.TrimSpace(lines[i]) 245 | 246 | if strings.HasPrefix(line, "Command:") { 247 | parsed.Command = strings.TrimSpace(strings.TrimPrefix(line, "Command:")) 248 | } else if strings.HasPrefix(line, "Suggested next steps:") { 249 | parsed.NextSteps = parseSuggestions(lines[i+1:]) 250 | break 251 | } 252 | } 253 | 254 | return parsed, nil 255 | } 256 | 257 | // parseSuggestions extracts the suggestions from the response lines after "Suggested next steps:". 258 | func parseSuggestions(lines []string) []string { 259 | var suggestions []string 260 | re := regexp.MustCompile(`^\d+\.\s+`) 261 | for _, line := range lines { 262 | line = strings.TrimSpace(line) 263 | // Match numbered suggestions (e.g., "1. Describe one of the pods") 264 | if matched := re.MatchString(line); matched { 265 | suggestions = append(suggestions, line) 266 | } 267 | } 268 | return suggestions 269 | } 270 | 271 | func stripMarkdownFences(s string) string { 272 | // A simple approach: remove any triple backticks 273 | // This regex will remove ``` and also handle possible variations like ```sql 274 | re := regexp.MustCompile("```(sql|bash|[a-zA-Z0-9]*)?") 275 | s = re.ReplaceAllString(s, "") 276 | return strings.ReplaceAll(s, "```", "") 277 | } 278 | -------------------------------------------------------------------------------- /pkg/ai/types.go: -------------------------------------------------------------------------------- 1 | package ai 2 | 3 | // AIModel defines the interface for different AI providers. 4 | type AIModel interface { 5 | // GetName returns the name of the AI model. 6 | GetName() string 7 | 8 | // GetAPIKey returns the API key for the AI model. 9 | GetAPIKey() string 10 | 11 | // GetCommand generates a command based on user input. 12 | GetCommand(prompt string) (*AIResponse, error) 13 | 14 | // SetContext sets the context for the AI model. 15 | SetContext(context string) 16 | 17 | // GetContext returns the context for the AI model. 18 | GetContext() string 19 | 20 | // SetCommandType sets the command type for the AI model. 21 | SetCommandType(commandType string) 22 | 23 | // GetCommandType returns the command type for the AI model. 24 | GetCommandType() string 25 | } 26 | 27 | // AIResponse holds the parsed command and suggested next steps. 28 | type AIResponse struct { 29 | // Prompt is the user prompt that is sent to the AI. 30 | Prompt string 31 | 32 | // Command is the command that is suggested by the AI. 33 | Command string 34 | 35 | // Answer is the answer that is provided by the AI. 36 | Answer string 37 | 38 | // NextSteps are the suggested next steps that are provided by the AI. 39 | NextSteps []string 40 | } 41 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/prompt-ops/pops/pkg/conn" 12 | ) 13 | 14 | // connections stores all the connection configurations in memory. 15 | var connections []conn.Connection 16 | 17 | // connectionsConfigFilePath defines the path to the connections configuration file. 18 | var connectionsConfigFilePath = getConfigFilePath("connections.json") 19 | 20 | // getConfigFilePath constructs the absolute path for the configuration file. 21 | func getConfigFilePath(filename string) string { 22 | homeDir, err := os.UserHomeDir() 23 | if err != nil { 24 | return filename 25 | } 26 | configDir := filepath.Join(homeDir, ".pops") 27 | if _, err := os.Stat(configDir); os.IsNotExist(err) { 28 | if err := os.MkdirAll(configDir, 0755); err != nil { 29 | return filename 30 | } 31 | } 32 | return filepath.Join(configDir, filename) 33 | } 34 | 35 | // init loads existing connections from the configuration file into memory. 36 | func init() { 37 | if err := loadConnections(); err != nil && !errors.Is(err, os.ErrNotExist) { 38 | fmt.Printf("Warning: Unable to load connections: %v\n", err) 39 | } 40 | } 41 | 42 | // loadConnections reads the connections from the JSON configuration file. 43 | func loadConnections() error { 44 | if _, statErr := os.Stat(connectionsConfigFilePath); os.IsNotExist(statErr) { 45 | if createErr := writeConnections(); createErr != nil { 46 | return createErr 47 | } 48 | } 49 | 50 | file, err := os.Open(connectionsConfigFilePath) 51 | if err != nil { 52 | return err 53 | } 54 | defer file.Close() 55 | 56 | var loadedConnections []conn.Connection 57 | if err := json.NewDecoder(file).Decode(&loadedConnections); err != nil { 58 | return err 59 | } 60 | 61 | connections = loadedConnections 62 | return nil 63 | } 64 | 65 | // SaveConnection saves a new connection or updates an existing one based on the connection name. 66 | func SaveConnection(conn conn.Connection) error { 67 | if connections == nil { 68 | if err := loadConnections(); err != nil && !errors.Is(err, os.ErrNotExist) { 69 | return err 70 | } 71 | } 72 | 73 | updated := false 74 | for i, existingConnection := range connections { 75 | if strings.EqualFold(existingConnection.Name, conn.Name) { 76 | connections[i] = conn 77 | updated = true 78 | break 79 | } 80 | } 81 | 82 | if !updated { 83 | connections = append(connections, conn) 84 | } 85 | 86 | if err := writeConnections(); err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | } 92 | 93 | // writeConnections writes the in-memory connections slice to the JSON configuration file. 94 | func writeConnections() error { 95 | file, err := os.Create(connectionsConfigFilePath) 96 | if err != nil { 97 | return err 98 | } 99 | defer file.Close() 100 | 101 | encoder := json.NewEncoder(file) 102 | encoder.SetIndent("", " ") 103 | if err := encoder.Encode(connections); err != nil { 104 | return err 105 | } 106 | 107 | return nil 108 | } 109 | 110 | // GetConnectionByName retrieves a connection by its name. 111 | func GetConnectionByName(connectionName string) (conn.Connection, error) { 112 | if connections == nil { 113 | if err := loadConnections(); err != nil { 114 | return conn.Connection{}, err 115 | } 116 | } 117 | 118 | for _, conn := range connections { 119 | if strings.EqualFold(conn.Name, connectionName) { 120 | return conn, nil 121 | } 122 | } 123 | 124 | return conn.Connection{}, fmt.Errorf("connection with name '%s' does not exist", connectionName) 125 | } 126 | 127 | // GetAllConnections retrieves all stored connections. 128 | func GetAllConnections() ([]conn.Connection, error) { 129 | if connections == nil { 130 | if err := loadConnections(); err != nil { 131 | return nil, err 132 | } 133 | } 134 | 135 | return connections, nil 136 | } 137 | 138 | // GetConnectionsByType retrieves connections filtered by their type. 139 | func GetConnectionsByType(connectionType string) ([]conn.Connection, error) { 140 | allConnections, err := GetAllConnections() 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | var filteredConnections []conn.Connection 146 | for _, conn := range allConnections { 147 | if strings.EqualFold(conn.Type.GetMainType(), connectionType) { 148 | filteredConnections = append(filteredConnections, conn) 149 | } 150 | } 151 | 152 | return filteredConnections, nil 153 | } 154 | 155 | // DeleteConnectionByName removes a connection by its name. 156 | func DeleteConnectionByName(connectionName string) error { 157 | if connections == nil { 158 | if err := loadConnections(); err != nil { 159 | return err 160 | } 161 | } 162 | 163 | var updatedConnections []conn.Connection 164 | found := false 165 | for _, conn := range connections { 166 | if strings.EqualFold(conn.Name, connectionName) { 167 | found = true 168 | continue 169 | } 170 | updatedConnections = append(updatedConnections, conn) 171 | } 172 | 173 | if !found { 174 | return fmt.Errorf("connection with name '%s' does not exist", connectionName) 175 | } 176 | 177 | connections = updatedConnections 178 | 179 | if err := writeConnections(); err != nil { 180 | return err 181 | } 182 | 183 | return nil 184 | } 185 | 186 | // DeleteAllConnections removes all stored connections. 187 | func DeleteAllConnections() error { 188 | connections = []conn.Connection{} 189 | 190 | if err := writeConnections(); err != nil { 191 | return err 192 | } 193 | 194 | return nil 195 | } 196 | 197 | func DeleteAllConnectionsByType(connectionType string) error { 198 | if connections == nil { 199 | if err := loadConnections(); err != nil { 200 | return err 201 | } 202 | } 203 | 204 | var updatedConnections []conn.Connection 205 | for _, conn := range connections { 206 | if !strings.EqualFold(conn.Type.GetMainType(), connectionType) { 207 | updatedConnections = append(updatedConnections, conn) 208 | } 209 | } 210 | 211 | connections = updatedConnections 212 | 213 | if err := writeConnections(); err != nil { 214 | return err 215 | } 216 | 217 | return nil 218 | } 219 | 220 | // CheckIfNameExists checks if a connection with the given name already exists. 221 | func CheckIfNameExists(name string) bool { 222 | if connections == nil { 223 | if err := loadConnections(); err != nil && !errors.Is(err, os.ErrNotExist) { 224 | return false 225 | } 226 | } 227 | 228 | for _, conn := range connections { 229 | if strings.EqualFold(conn.Name, name) { 230 | return true 231 | } 232 | } 233 | 234 | return false 235 | } 236 | -------------------------------------------------------------------------------- /pkg/conn/cloud.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "os/exec" 8 | "strings" 9 | 10 | "github.com/olekukonko/tablewriter" 11 | "github.com/prompt-ops/pops/pkg/ai" 12 | "golang.org/x/term" 13 | ) 14 | 15 | var ( 16 | // TODO: Rename? 17 | 18 | // AzureCloudConnection 19 | AzureCloudConnection = AvailableCloudConnectionType{ 20 | Subtype: "Azure", 21 | } 22 | 23 | // AWSCloudConnection 24 | AWSCloudConnection = AvailableCloudConnectionType{ 25 | Subtype: "AWS", 26 | } 27 | 28 | // GCPCloudConnection 29 | GCPCloudConnection = AvailableCloudConnectionType{ 30 | Subtype: "GCP", 31 | } 32 | 33 | // AvailableCloudConnectionTypes is a list of available cloud connections. 34 | AvailableCloudConnectionTypes = []AvailableCloudConnectionType{ 35 | AzureCloudConnection, 36 | AWSCloudConnection, 37 | GCPCloudConnection, 38 | } 39 | ) 40 | 41 | // AvailableCloudConnectionType is a helper struct to UI to list available cloud connection types. 42 | // Subtype will be shown in the UI. 43 | type AvailableCloudConnectionType struct { 44 | Subtype string 45 | } 46 | 47 | type CloudConnectionType struct { 48 | // MainType of the connection type. 49 | // Example: "cloud". 50 | MainType string `json:"mainType"` 51 | 52 | // Subtype of the cloud connection type. 53 | // Example: "aws", "gcp", "azure". 54 | Subtype string `json:"subtype"` 55 | } 56 | 57 | func (c CloudConnectionType) GetMainType() string { 58 | return "Cloud" 59 | } 60 | 61 | func (c CloudConnectionType) GetSubtype() string { 62 | return c.Subtype 63 | } 64 | 65 | type CloudConnectionDetails struct { 66 | } 67 | 68 | func (c CloudConnectionDetails) GetDriver() string { 69 | return "" 70 | } 71 | 72 | // NewCloudConnection creates a new cloud connection. 73 | func NewCloudConnection(name string, cloudProvider AvailableCloudConnectionType) Connection { 74 | return Connection{ 75 | Name: name, 76 | Type: CloudConnectionType{ 77 | MainType: "Cloud", 78 | Subtype: cloudProvider.Subtype, 79 | }, 80 | Details: CloudConnectionDetails{}, 81 | } 82 | } 83 | 84 | // GetCloudConnectionDetails retrieves the CloudConnectionDetails from a Connection. 85 | func GetCloudConnectionDetails(conn Connection) (CloudConnectionDetails, error) { 86 | if conn.Type.GetMainType() != ConnectionTypeCloud { 87 | return CloudConnectionDetails{}, fmt.Errorf("connection is not of type 'cloud'") 88 | } 89 | details, ok := conn.Details.(CloudConnectionDetails) 90 | if !ok { 91 | return CloudConnectionDetails{}, fmt.Errorf("invalid connection details for 'cloud'") 92 | } 93 | return details, nil 94 | } 95 | 96 | // BaseCloudConnection is a partial implementation of the ConnectionInterface for cloud. 97 | type BaseCloudConnection struct { 98 | Connection Connection 99 | } 100 | 101 | func (c *BaseCloudConnection) GetConnection() Connection { 102 | return c.Connection 103 | } 104 | 105 | func (c *BaseCloudConnection) ExecuteCommand(command string) ([]byte, error) { 106 | // Split the command into command and arguments 107 | // This is required for exec.Command 108 | // Example: "az group list" -> "az", "group", "list" 109 | parts := strings.Fields(command) 110 | if len(parts) == 0 { 111 | return nil, fmt.Errorf("no command provided") 112 | } 113 | 114 | // The first part is the command, the rest are the arguments 115 | cmd := exec.Command(parts[0], parts[1:]...) 116 | output, err := cmd.CombinedOutput() 117 | if err != nil { 118 | return nil, fmt.Errorf("failed to execute command: %v", err) 119 | } 120 | 121 | return output, nil 122 | } 123 | 124 | func (c *BaseCloudConnection) FormatResultAsTable(result []byte) (string, error) { 125 | var rows []map[string]interface{} 126 | if err := json.Unmarshal(result, &rows); err != nil { 127 | return "", fmt.Errorf("failed to parse JSON result: %v", err) 128 | } 129 | 130 | if len(rows) == 0 { 131 | return "No data available", nil 132 | } 133 | 134 | // Extract column headers (keys) from the first item 135 | var header []string 136 | for col := range rows[0] { 137 | header = append(header, col) 138 | } 139 | 140 | // Build slices of string data for each row 141 | var tableRows [][]string 142 | for _, row := range rows { 143 | var tableRow []string 144 | for _, col := range header { 145 | val := row[col] 146 | 147 | // Convert the value to a string. 148 | // Optionally truncate if it’s too long. 149 | strVal := fmt.Sprintf("%v", val) 150 | if len(strVal) > 60 { 151 | strVal = strVal[:57] + "..." 152 | } 153 | tableRow = append(tableRow, strVal) 154 | } 155 | tableRows = append(tableRows, tableRow) 156 | } 157 | 158 | // Get the screen width 159 | width, _, err := term.GetSize(0) 160 | if err != nil { 161 | width = 80 // default width if unable to get terminal size 162 | } 163 | 164 | // Calculate the column width 165 | numCols := len(header) 166 | colWidth := width / numCols 167 | 168 | // Build the table 169 | var buffer bytes.Buffer 170 | table := tablewriter.NewWriter(&buffer) 171 | 172 | // Tablewriter tweaks 173 | table.SetAutoWrapText(false) 174 | table.SetReflowDuringAutoWrap(false) 175 | table.SetRowLine(false) 176 | table.SetAutoFormatHeaders(false) 177 | table.SetAlignment(tablewriter.ALIGN_LEFT) 178 | 179 | // Set the calculated column width 180 | table.SetColWidth(colWidth) 181 | 182 | // Set headers 183 | table.SetHeader(header) 184 | 185 | // Add rows 186 | for _, row := range tableRows { 187 | table.Append(row) 188 | } 189 | 190 | // Render it! 191 | table.Render() 192 | 193 | return buffer.String(), nil 194 | } 195 | 196 | var _ ConnectionInterface = &AzureConnection{} 197 | 198 | type AzureConnection struct { 199 | BaseCloudConnection 200 | 201 | ResourceGroups []AzureResourceGroup 202 | } 203 | 204 | func (a *AzureConnection) CheckAuthentication() error { 205 | fmt.Println("Checking Azure authentication...") 206 | // Check if az cli is installed 207 | if _, err := exec.LookPath("az"); err != nil { 208 | return fmt.Errorf("az CLI is not installed") 209 | } 210 | 211 | // Check if az cli is logged in 212 | cmd := exec.Command("az", "account", "show") 213 | output, err := cmd.CombinedOutput() 214 | if err != nil { 215 | return fmt.Errorf("az CLI is not logged in: %v", string(output)) 216 | } 217 | 218 | return nil 219 | } 220 | 221 | // SetContext sets the context for the Azure connection. 222 | // This will populate the resource groups. 223 | func (a *AzureConnection) SetContext() error { 224 | // Get all resource groups 225 | resourceGroups, err := a.getResourceGroups() 226 | if err != nil { 227 | return err 228 | } 229 | 230 | a.ResourceGroups = resourceGroups 231 | return nil 232 | } 233 | 234 | // GetContext returns the resource groups in the Azure connection. 235 | func (a *AzureConnection) GetContext() string { 236 | if a.ResourceGroups == nil { 237 | // Call SetContext to populate the resource groups. 238 | // This is a fallback in case SetContext is not called. 239 | if err := a.SetContext(); err != nil { 240 | return fmt.Sprintf("Error getting context: %v", err) 241 | } 242 | } 243 | 244 | context := fmt.Sprintf("%s Connection Details:\n", a.Connection.Type.GetSubtype()) 245 | context += "Resource Groups:\n" 246 | 247 | for _, rg := range a.ResourceGroups { 248 | context += fmt.Sprintf("- %s\n", rg.Name) 249 | } 250 | 251 | return context 252 | } 253 | 254 | func (a *AzureConnection) GetFormattedContext() (string, error) { 255 | if a.ResourceGroups == nil { 256 | // Call SetContext to populate the resource groups. 257 | // This is a fallback in case SetContext is not called. 258 | if err := a.SetContext(); err != nil { 259 | return "", fmt.Errorf("error getting context: %v", err) 260 | } 261 | } 262 | 263 | var buffer bytes.Buffer 264 | table := tablewriter.NewWriter(&buffer) 265 | table.SetHeader([]string{"Resource Group"}) 266 | for _, rg := range a.ResourceGroups { 267 | table.Append([]string{rg.Name}) 268 | } 269 | table.Render() 270 | 271 | return buffer.String(), nil 272 | } 273 | 274 | func NewAzureConnection(connnection *Connection) *AzureConnection { 275 | return &AzureConnection{ 276 | BaseCloudConnection: BaseCloudConnection{ 277 | Connection: *connnection, 278 | }, 279 | } 280 | } 281 | 282 | func (a *AzureConnection) GetCommand(prompt string) (string, error) { 283 | if a.ResourceGroups == nil { 284 | // Call GetContext to populate the resource groups. 285 | // This is a fallback in case GetContext is not called. 286 | if err := a.SetContext(); err != nil { 287 | return "", fmt.Errorf("error getting context: %v", err) 288 | } 289 | } 290 | 291 | // Because this is the initial version of Prompt-Ops, 292 | // we are going to have overlaps like having context both 293 | // in the connection and in the AI model. 294 | // As we iterate on building Prompt-Ops, we will remove this overlap. 295 | aiModel, err := ai.NewOpenAIModel(a.CommandType(), a.GetContext()) 296 | if err != nil { 297 | return "", fmt.Errorf("failed to create AI model: %v", err) 298 | } 299 | 300 | cmd, err := aiModel.GetCommand(prompt) 301 | if err != nil { 302 | return "", fmt.Errorf("failed to get command from AI: %v", err) 303 | } 304 | 305 | return cmd.Command, nil 306 | } 307 | 308 | func (a *AzureConnection) GetAnswer(prompt string) (string, error) { 309 | if a.ResourceGroups == nil { 310 | // Call GetContext to populate the resource groups. 311 | // This is a fallback in case GetContext is not called. 312 | if err := a.SetContext(); err != nil { 313 | return "", fmt.Errorf("error getting context: %v", err) 314 | } 315 | } 316 | 317 | // Because this is the initial version of Prompt-Ops, 318 | // we are going to have overlaps like having context both 319 | // in the connection and in the AI model. 320 | // As we iterate on building Prompt-Ops, we will remove this overlap. 321 | aiModel, err := ai.NewOpenAIModel(a.CommandType(), a.GetContext()) 322 | if err != nil { 323 | return "", fmt.Errorf("failed to create AI model: %v", err) 324 | } 325 | 326 | answer, err := aiModel.GetAnswer(prompt) 327 | if err != nil { 328 | return "", fmt.Errorf("failed to get answer from AI: %v", err) 329 | } 330 | 331 | return answer.Answer, nil 332 | } 333 | 334 | func (a *AzureConnection) CommandType() string { 335 | return "az cli command" 336 | } 337 | 338 | // AzureResourceGroup represents an Azure resource group. 339 | type AzureResourceGroup struct { 340 | Name string `json:"name"` 341 | } 342 | 343 | // getResourceGroups gets all Azure resource groups. 344 | func (a *AzureConnection) getResourceGroups() ([]AzureResourceGroup, error) { 345 | cmd := exec.Command("az", "group", "list", "--output", "json") 346 | output, err := cmd.CombinedOutput() 347 | if err != nil { 348 | return nil, fmt.Errorf("failed to list resource groups: %v", string(output)) 349 | } 350 | 351 | var resourceGroups []AzureResourceGroup 352 | if err := json.Unmarshal(output, &resourceGroups); err != nil { 353 | return nil, fmt.Errorf("failed to parse resource groups: %v", err) 354 | } 355 | 356 | return resourceGroups, nil 357 | } 358 | -------------------------------------------------------------------------------- /pkg/conn/cloud_test.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestNewCloudConnection(t *testing.T) { 9 | type args struct { 10 | name string 11 | provider AvailableCloudConnectionType 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want Connection 17 | }{ 18 | { 19 | name: "Test NewCloudConnection", 20 | args: args{ 21 | name: "test", 22 | provider: AWSCloudConnection, 23 | }, 24 | want: Connection{ 25 | Name: "test", 26 | Type: CloudConnectionType{ 27 | MainType: ConnectionTypeCloud, 28 | Subtype: "AWS", 29 | }, 30 | Details: CloudConnectionDetails{}, 31 | }, 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | if got := NewCloudConnection(tt.args.name, tt.args.provider); !reflect.DeepEqual(got, tt.want) { 37 | t.Errorf("NewCloudConnection() = %v, want %v", got, tt.want) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestGetCloudConnectionDetails(t *testing.T) { 44 | type args struct { 45 | conn Connection 46 | } 47 | tests := []struct { 48 | name string 49 | args args 50 | want CloudConnectionDetails 51 | wantErr bool 52 | }{ 53 | { 54 | name: "Test GetCloudConnectionDetails", 55 | args: args{ 56 | conn: Connection{ 57 | Name: "test", 58 | Type: CloudConnectionType{ 59 | Subtype: "aws", 60 | }, 61 | Details: CloudConnectionDetails{}, 62 | }, 63 | }, 64 | want: CloudConnectionDetails{}, 65 | wantErr: false, 66 | }, 67 | { 68 | name: "Test GetCloudConnectionDetails with wrong type", 69 | args: args{ 70 | conn: Connection{ 71 | Name: "test", 72 | Type: DatabaseConnectionType{ 73 | Subtype: "postgres", 74 | }, 75 | Details: DatabaseConnectionDetails{ 76 | ConnectionString: "host=localhost user=test password=test dbname=test", 77 | Driver: "postgres", 78 | }, 79 | }, 80 | }, 81 | want: CloudConnectionDetails{}, 82 | wantErr: true, 83 | }, 84 | { 85 | name: "Test GetCloudConnectionDetails with wrong connection details", 86 | args: args{ 87 | conn: Connection{ 88 | Name: "test", 89 | Type: CloudConnectionType{ 90 | Subtype: "aws", 91 | }, 92 | Details: DatabaseConnectionDetails{ 93 | ConnectionString: "host=localhost user=test password=test dbname=test", 94 | Driver: "postgres", 95 | }, 96 | }, 97 | }, 98 | want: CloudConnectionDetails{}, 99 | wantErr: true, 100 | }, 101 | } 102 | for _, tt := range tests { 103 | t.Run(tt.name, func(t *testing.T) { 104 | got, err := GetCloudConnectionDetails(tt.args.conn) 105 | if (err != nil) != tt.wantErr { 106 | t.Errorf("GetCloudConnectionDetails() error = %v, wantErr %v", err, tt.wantErr) 107 | return 108 | } 109 | if !reflect.DeepEqual(got, tt.want) { 110 | t.Errorf("GetCloudConnectionDetails() = %v, want %v", got, tt.want) 111 | } 112 | }) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /pkg/conn/db_test.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestNewDatabaseConnection(t *testing.T) { 9 | type args struct { 10 | name string 11 | availableDatabaseConnection AvailableDatabaseConnectionType 12 | connectionString string 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want Connection 18 | }{ 19 | { 20 | name: "Test NewDatabaseConnection", 21 | args: args{ 22 | name: "test", 23 | availableDatabaseConnection: PostgreSQLDatabaseConnection, 24 | connectionString: "host=localhost user=test password=test dbname=test", 25 | }, 26 | want: Connection{ 27 | Name: "test", 28 | Type: DatabaseConnectionType{ 29 | MainType: ConnectionTypeDatabase, 30 | Subtype: "PostgreSQL", 31 | }, 32 | Details: DatabaseConnectionDetails{ 33 | ConnectionString: "host=localhost user=test password=test dbname=test", 34 | Driver: "postgres", 35 | }, 36 | }, 37 | }, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | if got := NewDatabaseConnection(tt.args.name, tt.args.availableDatabaseConnection, tt.args.connectionString); !reflect.DeepEqual(got, tt.want) { 42 | t.Errorf("NewDatabaseConnection() = %v, want %v", got, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestGetDatabaseConnectionDetails(t *testing.T) { 49 | type args struct { 50 | conn Connection 51 | } 52 | tests := []struct { 53 | name string 54 | args args 55 | want DatabaseConnectionDetails 56 | wantErr bool 57 | }{ 58 | { 59 | name: "Test GetDatabaseConnectionDetails", 60 | args: args{ 61 | conn: Connection{ 62 | Name: "test", 63 | Type: DatabaseConnectionType{ 64 | Subtype: "postgres", 65 | }, 66 | Details: DatabaseConnectionDetails{ 67 | ConnectionString: "host=localhost user=test password=test dbname=test", 68 | Driver: "postgres", 69 | }, 70 | }, 71 | }, 72 | want: DatabaseConnectionDetails{ 73 | ConnectionString: "host=localhost user=test password=test dbname=test", 74 | Driver: "postgres", 75 | }, 76 | wantErr: false, 77 | }, 78 | { 79 | name: "Test GetDatabaseConnectionDetails with wrong type", 80 | args: args{ 81 | conn: Connection{ 82 | Name: "test", 83 | Type: CloudConnectionType{ 84 | Subtype: "aws", 85 | }, 86 | Details: CloudConnectionDetails{}, 87 | }, 88 | }, 89 | want: DatabaseConnectionDetails{}, 90 | wantErr: true, 91 | }, 92 | { 93 | name: "Test GetDatabaseConnectionDetails with wrong connection details", 94 | args: args{ 95 | conn: Connection{ 96 | Name: "test", 97 | Type: DatabaseConnectionType{ 98 | Subtype: "postgres", 99 | }, 100 | Details: CloudConnectionDetails{}, 101 | }, 102 | }, 103 | want: DatabaseConnectionDetails{}, 104 | wantErr: true, 105 | }, 106 | } 107 | for _, tt := range tests { 108 | t.Run(tt.name, func(t *testing.T) { 109 | got, err := GetDatabaseConnectionDetails(tt.args.conn) 110 | if (err != nil) != tt.wantErr { 111 | t.Errorf("GetDatabaseConnectionDetails() error = %v, wantErr %v", err, tt.wantErr) 112 | return 113 | } 114 | if !reflect.DeepEqual(got, tt.want) { 115 | t.Errorf("GetDatabaseConnectionDetails() = %v, want %v", got, tt.want) 116 | } 117 | }) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /pkg/conn/factory.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Factory function to get the right implementation based on type and subtype 9 | func GetConnection(conn Connection) (ConnectionInterface, error) { 10 | switch conn.Type.GetMainType() { 11 | 12 | case ConnectionTypeCloud: 13 | switch strings.ToLower(conn.Type.GetSubtype()) { 14 | case "azure": 15 | return NewAzureConnection(&conn), nil 16 | default: 17 | return nil, fmt.Errorf("unsupported cloud subtype: %s", conn.Type.GetSubtype()) 18 | } 19 | 20 | case ConnectionTypeKubernetes: 21 | return NewKubernetesConnectionImpl(&conn), nil 22 | 23 | case ConnectionTypeDatabase: 24 | switch strings.ToLower(conn.Type.GetSubtype()) { 25 | case "postgresql": 26 | return NewPostgreSQLConnection(&conn), nil 27 | default: 28 | return nil, fmt.Errorf("unsupported database subtype: %s", conn.Type.GetSubtype()) 29 | } 30 | 31 | default: 32 | return nil, fmt.Errorf("[GetConnection] unsupported connection type: %s", conn.Type.GetSubtype()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/conn/kubernetes_test.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestNewKubernetesConnection(t *testing.T) { 9 | type args struct { 10 | name string 11 | context string 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want Connection 17 | }{ 18 | { 19 | name: "Test NewKubernetesConnection", 20 | args: args{ 21 | name: "test", 22 | context: "test-context", 23 | }, 24 | want: Connection{ 25 | Name: "test", 26 | Type: KubernetesConnectionType{ 27 | MainType: ConnectionTypeKubernetes, 28 | }, 29 | Details: KubernetesConnectionDetails{ 30 | SelectedContext: "test-context", 31 | }, 32 | }, 33 | }, 34 | } 35 | for _, tt := range tests { 36 | t.Run(tt.name, func(t *testing.T) { 37 | if got := NewKubernetesConnection(tt.args.name, tt.args.context); !reflect.DeepEqual(got, tt.want) { 38 | t.Errorf("NewKubernetesConnection() = %v, want %v", got, tt.want) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | func TestGetKubernetesConnectionDetails(t *testing.T) { 45 | type args struct { 46 | conn Connection 47 | } 48 | tests := []struct { 49 | name string 50 | args args 51 | want KubernetesConnectionDetails 52 | wantErr bool 53 | }{ 54 | { 55 | name: "Test GetKubernetesConnectionDetails", 56 | args: args{ 57 | conn: Connection{ 58 | Name: "test", 59 | Type: KubernetesConnectionType{}, 60 | Details: KubernetesConnectionDetails{ 61 | SelectedContext: "test-context", 62 | }, 63 | }, 64 | }, 65 | want: KubernetesConnectionDetails{ 66 | SelectedContext: "test-context", 67 | }, 68 | wantErr: false, 69 | }, 70 | { 71 | name: "Test GetKubernetesConnectionDetails with wrong type", 72 | args: args{ 73 | conn: Connection{ 74 | Name: "test", 75 | Type: DatabaseConnectionType{ 76 | Subtype: "postgres", 77 | }, 78 | Details: DatabaseConnectionDetails{ 79 | ConnectionString: "host=localhost user=test password=test dbname=test", 80 | Driver: "postgres", 81 | }, 82 | }, 83 | }, 84 | want: KubernetesConnectionDetails{}, 85 | wantErr: true, 86 | }, 87 | { 88 | name: "Test GetKubernetesConnectionDetails with wrong connection details", 89 | args: args{ 90 | conn: Connection{ 91 | Name: "test", 92 | Type: KubernetesConnectionType{}, 93 | Details: DatabaseConnectionDetails{ 94 | ConnectionString: "host=localhost user=test password=test dbname=test", 95 | Driver: "postgres", 96 | }, 97 | }, 98 | }, 99 | want: KubernetesConnectionDetails{}, 100 | wantErr: true, 101 | }, 102 | } 103 | for _, tt := range tests { 104 | t.Run(tt.name, func(t *testing.T) { 105 | got, err := GetKubernetesConnectionDetails(tt.args.conn) 106 | if (err != nil) != tt.wantErr { 107 | t.Errorf("GetKubernetesConnectionDetails() error = %v, wantErr %v", err, tt.wantErr) 108 | return 109 | } 110 | if !reflect.DeepEqual(got, tt.want) { 111 | t.Errorf("GetKubernetesConnectionDetails() = %v, want %v", got, tt.want) 112 | } 113 | }) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /pkg/conn/query.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | var TablesAndColumnsQueryMap = map[string]string{ 4 | "postgres": ` 5 | SELECT table_schema, table_name, column_name, data_type 6 | FROM information_schema.columns 7 | WHERE table_schema NOT IN ('information_schema', 'pg_catalog') 8 | ORDER BY table_schema, table_name, ordinal_position; 9 | `, 10 | "mysql": ` 11 | SELECT table_schema, table_name, column_name, data_type 12 | FROM information_schema.columns 13 | WHERE table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys') 14 | ORDER BY table_schema, table_name, ordinal_position; 15 | `, 16 | } 17 | -------------------------------------------------------------------------------- /pkg/conn/types.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | ConnectionTypeCloud = "Cloud" 10 | ConnectionTypeDatabase = "Database" 11 | ConnectionTypeKubernetes = "Kubernetes" 12 | ) 13 | 14 | // AvailableConnectionTypes is a list of available connection types. 15 | func AvailableConnectionTypes() []string { 16 | return []string{ 17 | ConnectionTypeCloud, 18 | ConnectionTypeDatabase, 19 | ConnectionTypeKubernetes, 20 | } 21 | } 22 | 23 | type ConnectionType interface { 24 | // GetMainType returns the main type of the connection. 25 | // Example: "database", "cloud", "kubernetes". 26 | GetMainType() string 27 | 28 | // GetSubtype returns the subtype of the connection. 29 | // Example: "postgres", "mysql", "aws", "gcp", "azure". 30 | // Can be empty if there is no subtype. 31 | GetSubtype() string 32 | } 33 | 34 | type ConnectionDetails interface { 35 | // GetDriver returns the driver name for the connection. 36 | // Example: "postgres", "mysql", "mongodb". 37 | // Can be empty if there is no driver. 38 | GetDriver() string 39 | } 40 | 41 | type Connection struct { 42 | // Name of the connection. 43 | Name string `json:"name"` 44 | 45 | // Type of the connection. 46 | // Example: "database", "cloud", "kubernetes". 47 | Type ConnectionType `json:"type"` 48 | 49 | // Details of the connection. 50 | // Can include different details based on the connection type. 51 | // Like database connection string, cloud credentials, etc. 52 | Details ConnectionDetails `json:"details"` 53 | } 54 | 55 | // UnmarshalJSON implements custom JSON decoding for the Connection struct. 56 | func (c *Connection) UnmarshalJSON(data []byte) error { 57 | // Create an alias to avoid infinite recursion when calling json.Unmarshal 58 | type alias Connection 59 | aux := &struct { 60 | Type json.RawMessage `json:"type"` 61 | Details json.RawMessage `json:"details"` 62 | *alias 63 | }{ 64 | alias: (*alias)(c), 65 | } 66 | 67 | if err := json.Unmarshal(data, &aux); err != nil { 68 | return err 69 | } 70 | 71 | // Unmarshal the Type field based on the main type 72 | var mainType struct { 73 | MainType string `json:"mainType"` 74 | } 75 | if err := json.Unmarshal(aux.Type, &mainType); err != nil { 76 | return err 77 | } 78 | 79 | switch mainType.MainType { 80 | case ConnectionTypeDatabase: 81 | var dbType DatabaseConnectionType 82 | if err := json.Unmarshal(aux.Type, &dbType); err != nil { 83 | return err 84 | } 85 | c.Type = dbType 86 | 87 | var dbDetails DatabaseConnectionDetails 88 | if err := json.Unmarshal(aux.Details, &dbDetails); err != nil { 89 | return err 90 | } 91 | c.Details = dbDetails 92 | 93 | case ConnectionTypeKubernetes: 94 | var k8sType KubernetesConnectionType 95 | if err := json.Unmarshal(aux.Type, &k8sType); err != nil { 96 | return err 97 | } 98 | c.Type = k8sType 99 | 100 | var k8sDetails KubernetesConnectionDetails 101 | if err := json.Unmarshal(aux.Details, &k8sDetails); err != nil { 102 | return err 103 | } 104 | c.Details = k8sDetails 105 | 106 | case ConnectionTypeCloud: 107 | var cloudType CloudConnectionType 108 | if err := json.Unmarshal(aux.Type, &cloudType); err != nil { 109 | return err 110 | } 111 | c.Type = cloudType 112 | 113 | var cloudDetails CloudConnectionDetails 114 | if err := json.Unmarshal(aux.Details, &cloudDetails); err != nil { 115 | return err 116 | } 117 | c.Details = cloudDetails 118 | 119 | default: 120 | return fmt.Errorf("unknown main type: %s", mainType.MainType) 121 | } 122 | 123 | return nil 124 | } 125 | 126 | type ConnectionInterface interface { 127 | GetConnection() Connection 128 | CheckAuthentication() error 129 | 130 | // SetContext gets the necessary information for the connection. 131 | // For example, for a database connection, it can get the list of tables and columns. 132 | // For a cloud connection, it can get the list of resources. 133 | // For a kubernetes connection, it can get the list of deployments, services, etc. 134 | // This information will be sent to the AI model which will use it to generate the queries/commands. 135 | SetContext() error 136 | 137 | // GetContext returns the information set by the SetContext method. 138 | // This information will be sent to the AI model which will use it to generate the queries/commands. 139 | GetContext() string 140 | 141 | // GetFormattedContext returns the formatted context for the AI model. 142 | GetFormattedContext() (string, error) 143 | 144 | // ExecuteCommand executes the given command and returns the output as byte array. 145 | ExecuteCommand(command string) ([]byte, error) 146 | 147 | // FormatResultAsTable formats the result as a table. 148 | FormatResultAsTable(result []byte) (string, error) 149 | 150 | // GetCommand gets the command from AI using context and the user prompt. 151 | GetCommand(prompt string) (string, error) 152 | 153 | // GetAnswer gets the answer from AI using context and the user prompt. 154 | GetAnswer(prompt string) (string, error) 155 | 156 | // CommandType returns the type of the command. 157 | // Example: "psql", "az", "kubectl". 158 | CommandType() string 159 | } 160 | -------------------------------------------------------------------------------- /pkg/ui/conn/cloud/create.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/charmbracelet/bubbles/spinner" 9 | "github.com/charmbracelet/bubbles/textinput" 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | config "github.com/prompt-ops/pops/pkg/config" 13 | "github.com/prompt-ops/pops/pkg/conn" 14 | "github.com/prompt-ops/pops/pkg/ui" 15 | ) 16 | 17 | // Styles 18 | var ( 19 | promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")) 20 | outputStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) 21 | ) 22 | 23 | const ( 24 | stepSelectProvider step = iota 25 | stepEnterConnectionName 26 | stepCreateSpinner 27 | stepCreateDone 28 | ) 29 | 30 | var providers = conn.AvailableCloudConnectionTypes 31 | 32 | type ( 33 | doneWaitingMsg struct { 34 | Connection conn.Connection 35 | } 36 | 37 | errMsg struct { 38 | err error 39 | } 40 | ) 41 | 42 | type createModel struct { 43 | currentStep step 44 | cursor int 45 | input textinput.Model 46 | err error 47 | spinner spinner.Model 48 | 49 | connection conn.Connection 50 | selectedCloudProvider conn.AvailableCloudConnectionType 51 | } 52 | 53 | func NewCreateModel() *createModel { 54 | ti := textinput.New() 55 | ti.Placeholder = ui.EnterConnectionNameMessage 56 | ti.CharLimit = 256 57 | ti.Width = 30 58 | 59 | sp := spinner.New() 60 | sp.Spinner = spinner.Dot 61 | 62 | return &createModel{ 63 | currentStep: stepSelectProvider, 64 | input: ti, 65 | spinner: sp, 66 | } 67 | } 68 | 69 | func (m *createModel) Init() tea.Cmd { 70 | // Make the text input blink by default 71 | return textinput.Blink 72 | } 73 | 74 | func (m *createModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 75 | var cmd tea.Cmd 76 | 77 | switch m.currentStep { 78 | 79 | //---------------------------------------------------------------------- 80 | // stepSelectProvider 81 | //---------------------------------------------------------------------- 82 | case stepSelectProvider: 83 | switch msg := msg.(type) { 84 | case tea.KeyMsg: 85 | switch msg.String() { 86 | case "up": 87 | if m.cursor > 0 { 88 | m.cursor-- 89 | } 90 | case "down": 91 | if m.cursor < len(providers)-1 { 92 | m.cursor++ 93 | } 94 | case "enter": 95 | m.selectedCloudProvider = providers[m.cursor] 96 | m.currentStep = stepEnterConnectionName 97 | m.input.Focus() 98 | m.err = nil 99 | return m, nil 100 | case "q", "esc", "ctrl+c": 101 | return m, tea.Quit 102 | } 103 | } 104 | 105 | //---------------------------------------------------------------------- 106 | // stepEnterConnectionName 107 | //---------------------------------------------------------------------- 108 | case stepEnterConnectionName: 109 | switch msg := msg.(type) { 110 | case tea.KeyMsg: 111 | m.input, cmd = m.input.Update(msg) 112 | switch msg.String() { 113 | case "enter": 114 | name := strings.TrimSpace(m.input.Value()) 115 | if name == "" { 116 | m.err = fmt.Errorf("connection name can't be empty") 117 | return m, nil 118 | } 119 | if config.CheckIfNameExists(name) { 120 | m.err = fmt.Errorf("connection name already exists") 121 | return m, nil 122 | } 123 | 124 | connection := conn.NewCloudConnection(name, m.selectedCloudProvider) 125 | if err := config.SaveConnection(connection); err != nil { 126 | m.err = err 127 | return m, nil 128 | } 129 | 130 | m.currentStep = stepCreateSpinner 131 | m.err = nil 132 | return m, tea.Batch( 133 | m.spinner.Tick, 134 | waitTwoSecondsCmd(connection), 135 | ) 136 | case "q", "esc", "ctrl+c": 137 | return m, tea.Quit 138 | } 139 | default: 140 | m.input, cmd = m.input.Update(msg) 141 | return m, cmd 142 | } 143 | 144 | //---------------------------------------------------------------------- 145 | // stepCreateSpinner 146 | //---------------------------------------------------------------------- 147 | case stepCreateSpinner: 148 | switch msg := msg.(type) { 149 | case spinner.TickMsg: 150 | m.spinner, cmd = m.spinner.Update(msg) 151 | return m, cmd 152 | case doneWaitingMsg: 153 | m.connection = msg.Connection 154 | m.currentStep = stepCreateDone 155 | m.err = nil 156 | return m, nil 157 | case errMsg: 158 | m.err = msg.err 159 | m.currentStep = stepCreateDone 160 | m.connection = conn.Connection{} 161 | return m, nil 162 | case tea.KeyMsg: 163 | switch msg.String() { 164 | case "q", "esc", "ctrl+c": 165 | return m, tea.Quit 166 | } 167 | } 168 | 169 | m.spinner, cmd = m.spinner.Update(msg) 170 | return m, cmd 171 | 172 | //---------------------------------------------------------------------- 173 | // stepCreateDone 174 | //---------------------------------------------------------------------- 175 | case stepCreateDone: 176 | switch msg := msg.(type) { 177 | case tea.KeyMsg: 178 | switch msg.String() { 179 | case "enter": 180 | return m, func() tea.Msg { 181 | return ui.TransitionToShellMsg{ 182 | Connection: m.connection, 183 | } 184 | } 185 | case "q", "esc", "ctrl+c": 186 | return m, tea.Quit 187 | } 188 | } 189 | } 190 | 191 | return m, cmd 192 | } 193 | 194 | func waitTwoSecondsCmd(conn conn.Connection) tea.Cmd { 195 | return tea.Tick(2*time.Second, func(t time.Time) tea.Msg { 196 | return doneWaitingMsg{ 197 | Connection: conn, 198 | } 199 | }) 200 | } 201 | 202 | func (m *createModel) View() string { 203 | // Clear the terminal before rendering the UI 204 | clearScreen := "\033[H\033[2J" 205 | 206 | switch m.currentStep { 207 | case stepSelectProvider: 208 | title := "Select a cloud provider (↑/↓, Enter to confirm):" 209 | footer := ui.QuitMessage 210 | 211 | var subtypeSelection string 212 | for i, p := range providers { 213 | cursor := " " 214 | if i == m.cursor { 215 | cursor = "→ " 216 | } 217 | subtypeSelection += fmt.Sprintf("%s%s\n", cursor, promptStyle.Render(p.Subtype)) 218 | } 219 | 220 | return fmt.Sprintf( 221 | "%s\n\n%s\n%s", 222 | titleStyle.Render(title), 223 | subtypeSelection, 224 | lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render(footer), 225 | ) 226 | 227 | case stepEnterConnectionName: 228 | title := "Enter a name for the Cloud connection:" 229 | footer := ui.QuitMessage 230 | 231 | if m.err != nil { 232 | errorMessage := fmt.Sprintf("Error: %v", m.err) 233 | 234 | return fmt.Sprintf( 235 | "%s\n\n%s\n\n%s\n\n%s", 236 | titleStyle.Render(title), 237 | errorStyle.Render(errorMessage), 238 | promptStyle.Render(m.input.View()), 239 | lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render(footer), 240 | ) 241 | } 242 | 243 | return fmt.Sprintf( 244 | "%s\n\n%s\n\n%s", 245 | titleStyle.Render(title), 246 | promptStyle.Render(m.input.View()), 247 | lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render(footer), 248 | ) 249 | 250 | case stepCreateSpinner: 251 | return clearScreen + outputStyle.Render("Saving connection... ") + m.spinner.View() 252 | 253 | case stepCreateDone: 254 | if m.err != nil { 255 | return clearScreen + errorStyle.Render(fmt.Sprintf("❌ Error: %v\n\nPress 'Enter' or 'q'/'esc' to quit.", m.err)) 256 | } 257 | 258 | return clearScreen + outputStyle.Render( 259 | "✅ Cloud connection created!\n\nPress 'Enter' or 'q'/'esc' to exit.", 260 | ) 261 | 262 | default: 263 | return clearScreen 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /pkg/ui/conn/cloud/create_test.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "testing" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestNewCreateModel(t *testing.T) { 12 | model := NewCreateModel() 13 | 14 | assert.Equal(t, stepSelectProvider, model.currentStep) 15 | assert.NotNil(t, model.input) 16 | assert.NotNil(t, model.spinner) 17 | } 18 | 19 | func TestCreateModel_View(t *testing.T) { 20 | model := NewCreateModel() 21 | 22 | // Test view for stepSelectProvider 23 | model.currentStep = stepSelectProvider 24 | view := model.View() 25 | assert.Contains(t, view, "Select a cloud provider") 26 | 27 | // Test view for stepEnterConnectionName 28 | model.currentStep = stepEnterConnectionName 29 | view = model.View() 30 | assert.Contains(t, view, "Enter a name for the Cloud connection") 31 | 32 | // Test view for stepCreateSpinner 33 | model.currentStep = stepCreateSpinner 34 | view = model.View() 35 | assert.Contains(t, view, "Saving connection") 36 | 37 | // Test view for stepCreateDone 38 | model.currentStep = stepCreateDone 39 | view = model.View() 40 | assert.Contains(t, view, "Cloud connection created") 41 | } 42 | 43 | func TestCreateModel_ErrorHandling(t *testing.T) { 44 | model := NewCreateModel() 45 | 46 | // Simulate entering an empty connection name 47 | model.currentStep = stepEnterConnectionName 48 | model.input.SetValue("") 49 | msg := tea.KeyMsg{Type: tea.KeyEnter} 50 | 51 | updatedModel, cmd := model.Update(msg) 52 | require.Nil(t, cmd) 53 | 54 | model = updatedModel.(*createModel) 55 | assert.Equal(t, stepEnterConnectionName, model.currentStep) 56 | assert.NotNil(t, model.err) 57 | assert.Contains(t, model.View(), "connection name can't be empty") 58 | } 59 | 60 | func TestCreateModel_Quit(t *testing.T) { 61 | model := NewCreateModel() 62 | 63 | // Simulate quitting at stepSelectProvider 64 | model.currentStep = stepSelectProvider 65 | msg := tea.KeyMsg{Type: tea.KeyEsc} 66 | 67 | _, cmd := model.Update(msg) 68 | require.NotNil(t, cmd) 69 | assert.Equal(t, tea.QuitMsg{}, cmd()) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/ui/conn/cloud/doc.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | // This package provides cloud UI components and utilities. 4 | -------------------------------------------------------------------------------- /pkg/ui/conn/cloud/open.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/bubbles/spinner" 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/lipgloss" 9 | config "github.com/prompt-ops/pops/pkg/config" 10 | "github.com/prompt-ops/pops/pkg/conn" 11 | "github.com/prompt-ops/pops/pkg/ui" 12 | ) 13 | 14 | // Styles 15 | var ( 16 | selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true) 17 | unselectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")) 18 | helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) 19 | ) 20 | 21 | const ( 22 | stepSelectConnection step = iota 23 | stepOpenSpinner 24 | stepOpenDone 25 | ) 26 | 27 | // Message types 28 | type ( 29 | // Sent when our spinner is done 30 | doneSpinnerMsg struct{} 31 | ) 32 | 33 | // model defines the state of the UI 34 | type model struct { 35 | currentStep step 36 | cursor int 37 | connections []conn.Connection 38 | selected conn.Connection 39 | err error 40 | spinner spinner.Model 41 | } 42 | 43 | // NewOpenModel initializes the open model for Cloud connections 44 | func NewOpenModel() model { 45 | sp := spinner.New() 46 | sp.Spinner = spinner.Dot 47 | 48 | return model{ 49 | currentStep: stepSelectConnection, 50 | spinner: sp, 51 | } 52 | } 53 | 54 | // Init initializes the model 55 | func (m model) Init() tea.Cmd { 56 | return tea.Batch( 57 | m.spinner.Tick, 58 | m.loadConnectionsCmd(), 59 | ) 60 | } 61 | 62 | // loadConnectionsCmd fetches existing cloud connections 63 | func (m model) loadConnectionsCmd() tea.Cmd { 64 | return func() tea.Msg { 65 | cloudConnections, err := config.GetConnectionsByType(conn.ConnectionTypeCloud) 66 | if err != nil { 67 | return err 68 | } 69 | if len(cloudConnections) == 0 { 70 | return fmt.Errorf("no cloud connections found") 71 | } 72 | return connectionsMsg{ 73 | connections: cloudConnections, 74 | } 75 | } 76 | } 77 | 78 | type connectionsMsg struct { 79 | connections []conn.Connection 80 | } 81 | 82 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 83 | var cmd tea.Cmd 84 | 85 | switch m.currentStep { 86 | case stepSelectConnection: 87 | switch msg := msg.(type) { 88 | case connectionsMsg: 89 | m.connections = msg.connections 90 | return m, nil 91 | case error: 92 | m.err = msg 93 | m.currentStep = stepOpenDone 94 | return m, nil 95 | case tea.KeyMsg: 96 | switch msg.String() { 97 | case "up", "k": 98 | if m.cursor > 0 { 99 | m.cursor-- 100 | } 101 | case "down", "j": 102 | if m.cursor < len(m.connections)-1 { 103 | m.cursor++ 104 | } 105 | case "enter": 106 | m.selected = m.connections[m.cursor] 107 | m.currentStep = stepOpenSpinner 108 | return m, tea.Batch( 109 | m.spinner.Tick, 110 | transitionCmd(m.selected), 111 | ) 112 | case "q", "esc", "ctrl+c": 113 | return m, tea.Quit 114 | } 115 | case spinner.TickMsg: 116 | m.spinner, cmd = m.spinner.Update(msg) 117 | return m, cmd 118 | } 119 | 120 | case stepOpenSpinner: 121 | switch msg := msg.(type) { 122 | case ui.TransitionToShellMsg: 123 | return m, tea.Quit 124 | case spinner.TickMsg: 125 | m.spinner, cmd = m.spinner.Update(msg) 126 | return m, cmd 127 | case doneSpinnerMsg: 128 | m.currentStep = stepOpenDone 129 | return m, nil 130 | } 131 | 132 | case stepOpenDone: 133 | switch msg := msg.(type) { 134 | case tea.KeyMsg: 135 | if msg.String() == "enter" || msg.String() == "q" || msg.String() == "esc" || msg.String() == "ctrl+c" { 136 | return m, tea.Quit 137 | } 138 | } 139 | } 140 | 141 | m.spinner, cmd = m.spinner.Update(msg) 142 | return m, cmd 143 | } 144 | 145 | func transitionCmd(conn conn.Connection) tea.Cmd { 146 | return func() tea.Msg { 147 | return ui.TransitionToShellMsg{ 148 | Connection: conn, 149 | } 150 | } 151 | } 152 | 153 | func (m model) View() string { 154 | // Clear the terminal before rendering the UI 155 | clearScreen := "\033[H\033[2J" 156 | 157 | switch m.currentStep { 158 | case stepSelectConnection: 159 | s := titleStyle.Render("Select a Cloud Connection (↑/↓, Enter to open):") 160 | s += "\n\n" 161 | for i, conn := range m.connections { 162 | cursor := " " 163 | if i == m.cursor { 164 | cursor = "→ " 165 | s += selectedStyle.Render(fmt.Sprintf("%s%s", cursor, conn.Name)) + "\n" 166 | continue 167 | } 168 | s += unselectedStyle.Render(fmt.Sprintf("%s%s", cursor, conn.Name)) + "\n" 169 | } 170 | s += "\n" + helpStyle.Render(ui.QuitMessage) 171 | return clearScreen + s 172 | 173 | case stepOpenSpinner: 174 | return clearScreen + lipgloss.JoinHorizontal(lipgloss.Left, 175 | fmt.Sprintf("Opening connection '%s'...", m.selected.Name), 176 | m.spinner.View(), 177 | ) 178 | 179 | case stepOpenDone: 180 | if m.err != nil { 181 | return clearScreen + errorStyle.Render(fmt.Sprintf("❌ Error: %v\n\nPress 'q' or 'esc' to quit.", m.err)) 182 | } 183 | return clearScreen + lipgloss.JoinHorizontal(lipgloss.Left, 184 | "✅ Connection opened!", 185 | "\n\nPress 'Enter' or 'q'/'esc' to exit.", 186 | ) 187 | default: 188 | return clearScreen 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /pkg/ui/conn/cloud/types.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import "github.com/charmbracelet/lipgloss" 4 | 5 | type step int 6 | 7 | var ( 8 | titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")) 9 | errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/ui/conn/db/create.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/charmbracelet/bubbles/spinner" 9 | "github.com/charmbracelet/bubbles/textinput" 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | config "github.com/prompt-ops/pops/pkg/config" 13 | "github.com/prompt-ops/pops/pkg/conn" 14 | "github.com/prompt-ops/pops/pkg/ui" 15 | ) 16 | 17 | var ( 18 | promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")) 19 | ) 20 | 21 | const ( 22 | stepSelectDriver step = iota 23 | stepEnterConnectionString 24 | stepEnterConnectionName 25 | stepCreateSpinner 26 | stepCreateDone 27 | ) 28 | 29 | var availableDatabaseConnections = conn.AvailableDatabaseConnectionTypes 30 | 31 | type ( 32 | doneWaitingMsg struct { 33 | Connection conn.Connection 34 | } 35 | 36 | errMsg struct { 37 | err error 38 | } 39 | ) 40 | 41 | type createModel struct { 42 | currentStep step 43 | cursor int 44 | 45 | connectionString string 46 | selectedDatabaseConnection conn.AvailableDatabaseConnectionType 47 | connection conn.Connection 48 | 49 | input textinput.Model 50 | connectionInput textinput.Model 51 | 52 | spinner spinner.Model 53 | 54 | err error 55 | } 56 | 57 | func NewCreateModel() *createModel { 58 | ti := textinput.New() 59 | ti.Placeholder = ui.EnterConnectionNameMessage 60 | ti.CharLimit = 256 61 | ti.Width = 30 62 | 63 | ci := textinput.New() 64 | ci.Placeholder = "Enter connection string..." 65 | ci.CharLimit = 512 66 | ci.Width = 50 67 | 68 | sp := spinner.New() 69 | sp.Spinner = spinner.Dot 70 | 71 | return &createModel{ 72 | currentStep: stepSelectDriver, 73 | cursor: 0, 74 | connectionString: "", 75 | input: ti, 76 | connectionInput: ci, 77 | spinner: sp, 78 | err: nil, 79 | } 80 | } 81 | 82 | func handleQuit(msg tea.KeyMsg) tea.Cmd { 83 | if msg.String() == "q" || msg.String() == "esc" || msg.String() == "ctrl+c" { 84 | return tea.Quit 85 | } 86 | return nil 87 | } 88 | 89 | func (m *createModel) Init() tea.Cmd { 90 | return tea.Batch(textinput.Blink, m.spinner.Tick) 91 | } 92 | 93 | func (m *createModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 94 | var ( 95 | cmd tea.Cmd 96 | cmds []tea.Cmd 97 | ) 98 | 99 | switch m.currentStep { 100 | case stepSelectDriver: 101 | switch msg := msg.(type) { 102 | case tea.KeyMsg: 103 | if quitCmd := handleQuit(msg); quitCmd != nil { 104 | return m, quitCmd 105 | } 106 | switch msg.String() { 107 | case "up": 108 | if m.cursor > 0 { 109 | m.cursor-- 110 | } 111 | case "down": 112 | if m.cursor < len(availableDatabaseConnections)-1 { 113 | m.cursor++ 114 | } 115 | case "enter": 116 | m.selectedDatabaseConnection = availableDatabaseConnections[m.cursor] 117 | m.currentStep = stepEnterConnectionString 118 | m.err = nil 119 | return m, m.connectionInput.Focus() 120 | } 121 | } 122 | 123 | case stepEnterConnectionString: 124 | switch msg := msg.(type) { 125 | case tea.KeyMsg: 126 | if quitCmd := handleQuit(msg); quitCmd != nil { 127 | return m, quitCmd 128 | } 129 | switch msg.String() { 130 | case "enter": 131 | connStr := strings.TrimSpace(m.connectionInput.Value()) 132 | if connStr == "" { 133 | m.err = fmt.Errorf("connection string can't be empty") 134 | return m, nil 135 | } 136 | m.connectionString = connStr 137 | m.currentStep = stepEnterConnectionName 138 | m.err = nil 139 | return m, m.input.Focus() 140 | } 141 | } 142 | m.connectionInput, cmd = m.connectionInput.Update(msg) 143 | cmds = append(cmds, cmd) 144 | return m, tea.Batch(cmds...) 145 | 146 | case stepEnterConnectionName: 147 | switch msg := msg.(type) { 148 | case tea.KeyMsg: 149 | if quitCmd := handleQuit(msg); quitCmd != nil { 150 | return m, quitCmd 151 | } 152 | switch msg.String() { 153 | case "enter": 154 | name := strings.TrimSpace(m.input.Value()) 155 | if name == "" { 156 | m.err = fmt.Errorf("connection name can't be empty") 157 | return m, nil 158 | } 159 | 160 | if config.CheckIfNameExists(name) { 161 | m.err = fmt.Errorf("connection name already exists") 162 | return m, nil 163 | } 164 | 165 | m.connection = conn.NewDatabaseConnection(name, m.selectedDatabaseConnection, m.connectionString) 166 | if err := config.SaveConnection(m.connection); err != nil { 167 | m.err = err 168 | m.currentStep = stepCreateDone 169 | return m, nil 170 | } 171 | 172 | m.currentStep = stepCreateSpinner 173 | m.err = nil 174 | return m, tea.Batch( 175 | m.spinner.Tick, 176 | waitTwoSecondsCmd(m.connection), 177 | ) 178 | } 179 | } 180 | m.input, cmd = m.input.Update(msg) 181 | cmds = append(cmds, cmd) 182 | return m, tea.Batch(cmds...) 183 | 184 | case stepCreateSpinner: 185 | switch msg := msg.(type) { 186 | case spinner.TickMsg: 187 | m.spinner, cmd = m.spinner.Update(msg) 188 | return m, cmd 189 | case doneWaitingMsg: 190 | m.connection = msg.Connection 191 | m.currentStep = stepCreateDone 192 | m.err = nil 193 | return m, nil 194 | case errMsg: 195 | m.err = msg.err 196 | m.currentStep = stepCreateDone 197 | m.connection = conn.Connection{} 198 | return m, nil 199 | case tea.KeyMsg: 200 | if quitCmd := handleQuit(msg); quitCmd != nil { 201 | return m, quitCmd 202 | } 203 | } 204 | m.spinner, cmd = m.spinner.Update(msg) 205 | cmds = append(cmds, cmd) 206 | return m, tea.Batch(cmds...) 207 | 208 | case stepCreateDone: 209 | switch msg := msg.(type) { 210 | case tea.KeyMsg: 211 | if quitCmd := handleQuit(msg); quitCmd != nil { 212 | return m, quitCmd 213 | } 214 | switch msg.String() { 215 | case "enter": 216 | return m, func() tea.Msg { 217 | return ui.TransitionToShellMsg{ 218 | Connection: m.connection, 219 | } 220 | } 221 | } 222 | } 223 | } 224 | 225 | switch msg := msg.(type) { 226 | case spinner.TickMsg: 227 | m.spinner, cmd = m.spinner.Update(msg) 228 | cmds = append(cmds, cmd) 229 | } 230 | 231 | return m, tea.Batch(cmds...) 232 | } 233 | 234 | func waitTwoSecondsCmd(conn conn.Connection) tea.Cmd { 235 | return tea.Tick(2*time.Second, func(t time.Time) tea.Msg { 236 | return doneWaitingMsg{ 237 | Connection: conn, 238 | } 239 | }) 240 | } 241 | 242 | func (m *createModel) View() string { 243 | // Clear the terminal before rendering the UI 244 | clearScreen := "\033[H\033[2J" 245 | 246 | switch m.currentStep { 247 | case stepSelectDriver: 248 | s := promptStyle.Render("Select a database driver (↑/↓, Enter to confirm):") 249 | s += "\n\n" 250 | for i, dbConn := range availableDatabaseConnections { 251 | cursor := " " 252 | if i == m.cursor { 253 | cursor = "→ " 254 | } 255 | if i == m.cursor { 256 | s += fmt.Sprintf("%s%s\n", cursor, promptStyle.Bold(true).Render(dbConn.Subtype)) 257 | } else { 258 | s += fmt.Sprintf("%s%s\n", cursor, promptStyle.Render(dbConn.Subtype)) 259 | } 260 | } 261 | s += "\nPress 'q', 'esc', or Ctrl+C to quit." 262 | return clearScreen + s 263 | 264 | case stepEnterConnectionString: 265 | s := promptStyle.Render("Enter the connection string:") 266 | s += "\n\n" 267 | if m.err != nil { 268 | s += fmt.Sprintf("Error: %v", m.err) 269 | s += "\n\n" 270 | } 271 | s += m.connectionInput.View() 272 | s += "\n\nPress 'Enter' to proceed or 'q', 'esc' to quit." 273 | return clearScreen + s 274 | 275 | case stepEnterConnectionName: 276 | s := promptStyle.Render("Enter a name for the database connection:") 277 | s += "\n\n" 278 | if m.err != nil { 279 | s += fmt.Sprintf("Error: %v", m.err) 280 | s += "\n\n" 281 | } 282 | s += m.input.View() 283 | s += "\n\nPress 'Enter' to save or 'q', 'esc' to quit." 284 | return clearScreen + s 285 | 286 | case stepCreateSpinner: 287 | if m.err != nil { 288 | return clearScreen + fmt.Sprintf("❌ Error: %v\n\nPress 'q', 'esc', or Ctrl+C to quit.", m.err) 289 | } 290 | return clearScreen + fmt.Sprintf("Saving connection... %s", m.spinner.View()) 291 | 292 | case stepCreateDone: 293 | if m.err != nil { 294 | return clearScreen + fmt.Sprintf("❌ Error: %v\n\nPress 'q', 'esc', or Ctrl+C to quit.", m.err) 295 | } 296 | 297 | return clearScreen + "✅ Database connection created!\n\nPress 'Enter' to continue or 'q', 'esc' to quit." 298 | 299 | default: 300 | return clearScreen 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /pkg/ui/conn/db/doc.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | // This package provides DB UI components and utilities. 4 | -------------------------------------------------------------------------------- /pkg/ui/conn/db/open.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/bubbles/spinner" 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/lipgloss" 9 | config "github.com/prompt-ops/pops/pkg/config" 10 | "github.com/prompt-ops/pops/pkg/conn" 11 | "github.com/prompt-ops/pops/pkg/ui" 12 | ) 13 | 14 | var ( 15 | selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true) 16 | unselectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")) 17 | helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) 18 | ) 19 | 20 | const ( 21 | stepSelectConnection step = iota 22 | stepOpenSpinner 23 | stepOpenDone 24 | ) 25 | 26 | type ( 27 | doneSpinnerMsg struct{} 28 | ) 29 | 30 | type model struct { 31 | currentStep step 32 | cursor int 33 | connections []conn.Connection 34 | selected conn.Connection 35 | err error 36 | spinner spinner.Model 37 | } 38 | 39 | // NewOpenModel returns a new database open model 40 | func NewOpenModel() model { 41 | sp := spinner.New() 42 | sp.Spinner = spinner.Dot 43 | 44 | return model{ 45 | currentStep: stepSelectConnection, 46 | spinner: sp, 47 | } 48 | } 49 | 50 | func (m model) Init() tea.Cmd { 51 | return tea.Batch( 52 | m.spinner.Tick, 53 | m.loadConnectionsCmd(), 54 | ) 55 | } 56 | 57 | // loadConnectionsCmd fetches existing database connections 58 | func (m model) loadConnectionsCmd() tea.Cmd { 59 | return func() tea.Msg { 60 | databaseConnections, err := config.GetConnectionsByType(conn.ConnectionTypeDatabase) 61 | if err != nil { 62 | return err 63 | } 64 | if len(databaseConnections) == 0 { 65 | return fmt.Errorf("no database connections found") 66 | } 67 | return connectionsMsg{ 68 | connections: databaseConnections, 69 | } 70 | } 71 | } 72 | 73 | // connectionsMsg holds the list of database connections 74 | type connectionsMsg struct { 75 | connections []conn.Connection 76 | } 77 | 78 | // Update handles incoming messages and updates the model accordingly 79 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 80 | var cmd tea.Cmd 81 | 82 | switch m.currentStep { 83 | case stepSelectConnection: 84 | switch msg := msg.(type) { 85 | case connectionsMsg: 86 | m.connections = msg.connections 87 | return m, nil 88 | case error: 89 | m.err = msg 90 | m.currentStep = stepOpenDone 91 | return m, nil 92 | case tea.KeyMsg: 93 | switch msg.String() { 94 | case "up", "k": 95 | if m.cursor > 0 { 96 | m.cursor-- 97 | } 98 | case "down", "j": 99 | if m.cursor < len(m.connections)-1 { 100 | m.cursor++ 101 | } 102 | case "enter": 103 | m.selected = m.connections[m.cursor] 104 | m.currentStep = stepOpenSpinner 105 | return m, tea.Batch( 106 | m.spinner.Tick, 107 | transitionCmd(m.selected), 108 | ) 109 | case "q", "esc", "ctrl+c": 110 | return m, tea.Quit 111 | } 112 | case spinner.TickMsg: 113 | m.spinner, cmd = m.spinner.Update(msg) 114 | return m, cmd 115 | } 116 | 117 | case stepOpenSpinner: 118 | switch msg := msg.(type) { 119 | case ui.TransitionToShellMsg: 120 | return m, tea.Quit 121 | case spinner.TickMsg: 122 | m.spinner, cmd = m.spinner.Update(msg) 123 | return m, cmd 124 | case doneSpinnerMsg: 125 | m.currentStep = stepOpenDone 126 | return m, nil 127 | } 128 | 129 | case stepOpenDone: 130 | switch msg := msg.(type) { 131 | case tea.KeyMsg: 132 | if msg.String() == "enter" || msg.String() == "q" || msg.String() == "esc" || msg.String() == "ctrl+c" { 133 | return m, tea.Quit 134 | } 135 | } 136 | } 137 | 138 | m.spinner, cmd = m.spinner.Update(msg) 139 | return m, cmd 140 | } 141 | 142 | // transitionCmd sends the TransitionToShellMsg after spinner 143 | func transitionCmd(conn conn.Connection) tea.Cmd { 144 | return func() tea.Msg { 145 | return ui.TransitionToShellMsg{ 146 | Connection: conn, 147 | } 148 | } 149 | } 150 | 151 | // View renders the UI based on the current step 152 | func (m model) View() string { 153 | // Clear the terminal before rendering the UI 154 | clearScreen := "\033[H\033[2J" 155 | 156 | switch m.currentStep { 157 | case stepSelectConnection: 158 | s := titleStyle.Render("Select a database connection (↑/↓, Enter to open):") 159 | s += "\n\n" 160 | for i, conn := range m.connections { 161 | cursor := " " 162 | if i == m.cursor { 163 | cursor = "→ " 164 | s += selectedStyle.Render(fmt.Sprintf("%s%s", cursor, conn.Name)) + "\n" 165 | continue 166 | } 167 | s += unselectedStyle.Render(fmt.Sprintf("%s%s", cursor, conn.Name)) + "\n" 168 | } 169 | s += "\n" + helpStyle.Render(ui.QuitMessage) 170 | return clearScreen + s 171 | 172 | case stepOpenSpinner: 173 | return clearScreen + lipgloss.JoinHorizontal(lipgloss.Left, 174 | fmt.Sprintf("Opening connection '%s'...", m.selected.Name), 175 | m.spinner.View(), 176 | ) 177 | 178 | case stepOpenDone: 179 | if m.err != nil { 180 | return clearScreen + errorStyle.Render(fmt.Sprintf("❌ Error: %v\n\nPress 'q' or 'esc' to quit.", m.err)) 181 | } 182 | return clearScreen + lipgloss.JoinHorizontal(lipgloss.Left, 183 | "✅ Connection opened!", 184 | "\n\nPress 'Enter' to start the shell or 'q'/'esc' to exit.", 185 | ) 186 | default: 187 | return clearScreen 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /pkg/ui/conn/db/types.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "github.com/charmbracelet/lipgloss" 4 | 5 | type step int 6 | 7 | var ( 8 | titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")) 9 | errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/ui/conn/k8s/create.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | "time" 8 | 9 | "github.com/charmbracelet/bubbles/spinner" 10 | "github.com/charmbracelet/bubbles/textinput" 11 | tea "github.com/charmbracelet/bubbletea" 12 | "github.com/charmbracelet/lipgloss" 13 | config "github.com/prompt-ops/pops/pkg/config" 14 | "github.com/prompt-ops/pops/pkg/conn" 15 | "github.com/prompt-ops/pops/pkg/ui" 16 | ) 17 | 18 | // Styles 19 | var ( 20 | outputStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) 21 | ) 22 | 23 | const ( 24 | stepSelectContext step = iota 25 | stepEnterConnectionName 26 | stepCreateSpinner 27 | stepCreateDone 28 | ) 29 | 30 | type ( 31 | doneWaitingMsg struct { 32 | Connection conn.Connection 33 | } 34 | 35 | contextsMsg struct { 36 | contexts []string 37 | } 38 | 39 | errMsg struct { 40 | err error 41 | } 42 | ) 43 | 44 | // createModel defines the state of the UI 45 | type createModel struct { 46 | currentStep step 47 | cursor int 48 | contexts []string 49 | selectedCtx string 50 | input textinput.Model 51 | err error 52 | 53 | // Spinner for the 2-second wait 54 | spinner spinner.Model 55 | 56 | connection conn.Connection 57 | } 58 | 59 | // NewCreateModel initializes the createModel for Kubernetes 60 | func NewCreateModel() *createModel { 61 | ti := textinput.New() 62 | ti.Placeholder = ui.EnterConnectionNameMessage 63 | ti.CharLimit = 256 64 | ti.Width = 30 65 | 66 | sp := spinner.New() 67 | sp.Spinner = spinner.Dot 68 | 69 | return &createModel{ 70 | currentStep: stepSelectContext, 71 | input: ti, 72 | spinner: sp, 73 | } 74 | } 75 | 76 | // Init initializes the createModel 77 | func (m *createModel) Init() tea.Cmd { 78 | return tea.Batch( 79 | m.loadContextsCmd(), 80 | ) 81 | } 82 | 83 | // loadContextsCmd fetches available Kubernetes contexts 84 | func (m *createModel) loadContextsCmd() tea.Cmd { 85 | return func() tea.Msg { 86 | out, err := exec.Command("kubectl", "config", "get-contexts", "--output=name").Output() 87 | if err != nil { 88 | return errMsg{err} 89 | } 90 | contextList := strings.Split(strings.TrimSpace(string(out)), "\n") 91 | return contextsMsg{contexts: contextList} 92 | } 93 | } 94 | 95 | // waitTwoSecondsCmd simulates a delay for saving the connection asynchronously 96 | func waitTwoSecondsCmd(conn conn.Connection) tea.Cmd { 97 | return tea.Tick(2*time.Second, func(t time.Time) tea.Msg { 98 | return doneWaitingMsg{ 99 | Connection: conn, 100 | } 101 | }) 102 | } 103 | 104 | // Update handles incoming messages and updates the createModel accordingly 105 | func (m *createModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 106 | var cmd tea.Cmd 107 | 108 | switch m.currentStep { 109 | case stepSelectContext: 110 | switch msg := msg.(type) { 111 | case contextsMsg: 112 | m.contexts = msg.contexts 113 | if len(m.contexts) == 0 { 114 | m.err = fmt.Errorf("no Kubernetes contexts found") 115 | m.currentStep = stepCreateSpinner 116 | return m, nil 117 | } 118 | // Clear any previous errors when successfully loading contexts 119 | m.err = nil 120 | return m, nil 121 | case errMsg: 122 | m.err = msg.err 123 | m.currentStep = stepCreateSpinner 124 | return m, nil 125 | case tea.KeyMsg: 126 | switch msg.String() { 127 | case "up", "k": 128 | if m.cursor > 0 { 129 | m.cursor-- 130 | } 131 | case "down", "j": 132 | if m.cursor < len(m.contexts)-1 { 133 | m.cursor++ 134 | } 135 | case "enter": 136 | if len(m.contexts) > 0 && m.cursor >= 0 && m.cursor < len(m.contexts) { 137 | m.selectedCtx = m.contexts[m.cursor] 138 | m.currentStep = stepEnterConnectionName 139 | m.input.Focus() 140 | 141 | // Clear any previous errors when moving to a new step 142 | m.err = nil 143 | 144 | return m, nil 145 | } 146 | case "q", "esc", "ctrl+c": 147 | return m, tea.Quit 148 | } 149 | } 150 | 151 | // Update spinner if it's running in stepSelectContext 152 | m.spinner, cmd = m.spinner.Update(msg) 153 | return m, cmd 154 | 155 | case stepEnterConnectionName: 156 | switch msg := msg.(type) { 157 | case tea.KeyMsg: 158 | switch msg.String() { 159 | case "enter": 160 | name := strings.TrimSpace(m.input.Value()) 161 | if name == "" { 162 | m.err = fmt.Errorf("connection name can't be empty") 163 | return m, nil 164 | } 165 | if config.CheckIfNameExists(name) { 166 | m.err = fmt.Errorf("connection name already exists") 167 | return m, nil 168 | } 169 | 170 | connection := conn.NewKubernetesConnection(name, m.selectedCtx) 171 | if err := config.SaveConnection(connection); err != nil { 172 | m.err = err 173 | return m, nil 174 | } 175 | m.currentStep = stepCreateSpinner 176 | m.err = nil 177 | return m, tea.Batch( 178 | m.spinner.Tick, 179 | waitTwoSecondsCmd(connection), 180 | ) 181 | case "q", "esc", "ctrl+c": 182 | return m, tea.Quit 183 | } 184 | case spinner.TickMsg: 185 | return m, nil 186 | } 187 | 188 | m.input, cmd = m.input.Update(msg) 189 | return m, cmd 190 | 191 | case stepCreateSpinner: 192 | switch msg := msg.(type) { 193 | case spinner.TickMsg: 194 | m.spinner, cmd = m.spinner.Update(msg) 195 | return m, cmd 196 | 197 | case doneWaitingMsg: 198 | m.connection = msg.Connection 199 | m.currentStep = stepCreateDone 200 | m.err = nil 201 | return m, nil 202 | 203 | case errMsg: 204 | m.err = msg.err 205 | m.currentStep = stepCreateDone 206 | m.connection = conn.Connection{} 207 | return m, nil 208 | 209 | case tea.KeyMsg: 210 | switch msg.String() { 211 | case "q", "esc", "ctrl+c": 212 | return m, tea.Quit 213 | } 214 | } 215 | 216 | m.spinner, cmd = m.spinner.Update(msg) 217 | return m, cmd 218 | 219 | case stepCreateDone: 220 | switch msg := msg.(type) { 221 | case tea.KeyMsg: 222 | switch msg.String() { 223 | case "enter": 224 | return m, func() tea.Msg { 225 | return ui.TransitionToShellMsg{ 226 | Connection: m.connection, 227 | } 228 | } 229 | case "q", "esc", "ctrl+c": 230 | return m, tea.Quit 231 | } 232 | case spinner.TickMsg: 233 | return m, nil 234 | } 235 | } 236 | 237 | return m, cmd 238 | } 239 | 240 | func (m *createModel) View() string { 241 | // Clear the terminal before rendering the UI 242 | clearScreen := "\033[H\033[2J" 243 | 244 | switch m.currentStep { 245 | case stepSelectContext: 246 | s := titleStyle.Render("Select a Kubernetes context (↑/↓, Enter to confirm):") 247 | s += "\n\n" 248 | for i, ctx := range m.contexts { 249 | cursor := " " 250 | if i == m.cursor { 251 | cursor = "→ " 252 | s += selectedStyle.Render(cursor+ctx) + "\n" 253 | continue 254 | } 255 | s += unselectedStyle.Render(cursor+ctx) + "\n" 256 | } 257 | s += "\n" + helpStyle.Render(ui.QuitMessage) 258 | return clearScreen + s 259 | 260 | case stepEnterConnectionName: 261 | s := titleStyle.Render("Enter a name for the Kubernetes connection:") 262 | s += "\n\n" 263 | if m.err != nil { 264 | s += errorStyle.Render(fmt.Sprintf("Error: %v", m.err)) 265 | s += "\n" 266 | } 267 | s += m.input.View() 268 | s += "\n" + helpStyle.Render(ui.QuitMessage) 269 | return clearScreen + s 270 | 271 | case stepCreateSpinner: 272 | return clearScreen + outputStyle.Render("Saving connection... ") + m.spinner.View() 273 | 274 | case stepCreateDone: 275 | if m.err != nil { 276 | return clearScreen + errorStyle.Render(fmt.Sprintf("❌ Error: %v\n\nPress 'Enter' or 'q'/'esc' to quit.", m.err)) 277 | } 278 | 279 | return clearScreen + outputStyle.Render("✅ Kubernetes connection created!\n\nPress 'Enter' or 'q'/'esc' to exit.") 280 | 281 | default: 282 | return clearScreen 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /pkg/ui/conn/k8s/doc.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | // This package provides Kubernetes UI components and utilities. 4 | -------------------------------------------------------------------------------- /pkg/ui/conn/k8s/open.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/bubbles/spinner" 7 | tea "github.com/charmbracelet/bubbletea" 8 | config "github.com/prompt-ops/pops/pkg/config" 9 | "github.com/prompt-ops/pops/pkg/conn" 10 | "github.com/prompt-ops/pops/pkg/ui" 11 | ) 12 | 13 | const ( 14 | stepSelectConnection step = iota 15 | stepOpenSpinner 16 | stepOpenDone 17 | ) 18 | 19 | // Message types 20 | type ( 21 | // Sent when our spinner is done 22 | doneSpinnerMsg struct{} 23 | ) 24 | 25 | // openModel defines the state of the UI 26 | type openModel struct { 27 | currentStep step 28 | cursor int 29 | connections []conn.Connection 30 | selected conn.Connection 31 | err error 32 | 33 | // Spinner for transitions 34 | spinner spinner.Model 35 | } 36 | 37 | // NewOpenModel initializes the open openModel for Kubernetes connections 38 | func NewOpenModel() *openModel { 39 | sp := spinner.New() 40 | sp.Spinner = spinner.Dot 41 | 42 | return &openModel{ 43 | currentStep: stepSelectConnection, 44 | spinner: sp, 45 | } 46 | } 47 | 48 | // Init initializes the openModel 49 | func (m *openModel) Init() tea.Cmd { 50 | return tea.Batch( 51 | m.spinner.Tick, 52 | m.loadConnectionsCmd(), 53 | ) 54 | } 55 | 56 | // loadConnectionsCmd fetches existing Kubernetes connections 57 | func (m *openModel) loadConnectionsCmd() tea.Cmd { 58 | return func() tea.Msg { 59 | k8sConnections, err := config.GetConnectionsByType(conn.ConnectionTypeKubernetes) 60 | if err != nil { 61 | return err 62 | } 63 | if len(k8sConnections) == 0 { 64 | return fmt.Errorf("no Kubernetes connections found") 65 | } 66 | return connectionsMsg{ 67 | connections: k8sConnections, 68 | } 69 | } 70 | } 71 | 72 | // connectionsMsg holds the list of Kubernetes connections 73 | type connectionsMsg struct { 74 | connections []conn.Connection 75 | } 76 | 77 | // Update handles incoming messages and updates the openModel accordingly 78 | func (m *openModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 79 | var cmd tea.Cmd 80 | 81 | switch m.currentStep { 82 | case stepSelectConnection: 83 | switch msg := msg.(type) { 84 | case connectionsMsg: 85 | m.connections = msg.connections 86 | return m, nil 87 | case error: 88 | m.err = msg 89 | m.currentStep = stepOpenDone 90 | return m, nil 91 | case tea.KeyMsg: 92 | switch msg.String() { 93 | case "up", "k": 94 | if m.cursor > 0 { 95 | m.cursor-- 96 | } 97 | case "down", "j": 98 | if m.cursor < len(m.connections)-1 { 99 | m.cursor++ 100 | } 101 | case "enter": 102 | m.selected = m.connections[m.cursor] 103 | m.currentStep = stepOpenSpinner 104 | return m, tea.Batch( 105 | m.spinner.Tick, 106 | transitionCmd(m.selected), 107 | ) 108 | case "q", "esc", "ctrl+c": 109 | return m, tea.Quit 110 | } 111 | case spinner.TickMsg: 112 | m.spinner, cmd = m.spinner.Update(msg) 113 | return m, cmd 114 | } 115 | 116 | case stepOpenSpinner: 117 | switch msg := msg.(type) { 118 | case ui.TransitionToShellMsg: 119 | return m, tea.Quit 120 | case spinner.TickMsg: 121 | m.spinner, cmd = m.spinner.Update(msg) 122 | return m, cmd 123 | case doneSpinnerMsg: 124 | m.currentStep = stepOpenDone 125 | return m, nil 126 | } 127 | 128 | case stepOpenDone: 129 | switch msg := msg.(type) { 130 | case tea.KeyMsg: 131 | switch msg.String() { 132 | case "enter", "q", "esc", "ctrl+c": 133 | return m, tea.Quit 134 | } 135 | } 136 | } 137 | 138 | // Always update the spinner 139 | m.spinner, cmd = m.spinner.Update(msg) 140 | return m, cmd 141 | } 142 | 143 | func transitionCmd(conn conn.Connection) tea.Cmd { 144 | return func() tea.Msg { 145 | return ui.TransitionToShellMsg{ 146 | Connection: conn, 147 | } 148 | } 149 | } 150 | 151 | func (m *openModel) View() string { 152 | // Clear the terminal before rendering the UI 153 | clearScreen := "\033[H\033[2J" 154 | 155 | switch m.currentStep { 156 | case stepSelectConnection: 157 | s := titleStyle.Render("Select a Kubernetes Connection (↑/↓, Enter to open):") 158 | s += "\n\n" 159 | for i, conn := range m.connections { 160 | cursor := " " 161 | if i == m.cursor { 162 | cursor = "→ " 163 | s += selectedStyle.Render(fmt.Sprintf("%s%s", cursor, conn.Name)) + "\n" 164 | continue 165 | } 166 | s += unselectedStyle.Render(fmt.Sprintf("%s%s", cursor, conn.Name)) + "\n" 167 | } 168 | s += "\n" + helpStyle.Render(ui.QuitMessage) 169 | return clearScreen + s 170 | 171 | case stepOpenSpinner: 172 | return clearScreen + fmt.Sprintf("Opening connection '%s'... %s", m.selected.Name, m.spinner.View()) 173 | 174 | case stepOpenDone: 175 | if m.err != nil { 176 | return clearScreen + errorStyle.Render(fmt.Sprintf("❌ Error: %v\n\nPress 'q' or 'esc' to quit.", m.err)) 177 | } 178 | return clearScreen + "✅ Connection opened!\n\nPress 'Enter' or 'q'/'esc' to exit." 179 | 180 | default: 181 | return clearScreen 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /pkg/ui/conn/k8s/types.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import "github.com/charmbracelet/lipgloss" 4 | 5 | type step int 6 | 7 | var ( 8 | titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")) 9 | errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("9")) 10 | selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")).Bold(true) 11 | unselectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")) 12 | helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/ui/conn/open.go: -------------------------------------------------------------------------------- 1 | package conn 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/prompt-ops/pops/pkg/config" 7 | "github.com/prompt-ops/pops/pkg/ui" 8 | "github.com/prompt-ops/pops/pkg/ui/shell" 9 | 10 | "github.com/charmbracelet/bubbles/table" 11 | tea "github.com/charmbracelet/bubbletea" 12 | "github.com/charmbracelet/lipgloss" 13 | "github.com/fatih/color" 14 | ) 15 | 16 | type openRootModel struct { 17 | step openStep 18 | tableModel tableModel 19 | shellModel tea.Model 20 | } 21 | 22 | type openStep int 23 | 24 | const ( 25 | stepPick openStep = iota 26 | stepShell 27 | ) 28 | 29 | type tableModel interface { 30 | tea.Model 31 | Selected() string 32 | } 33 | 34 | // NewOpenRootModel initializes the openRootModel with connections. 35 | func NewOpenRootModel() *openRootModel { 36 | connections, err := config.GetAllConnections() 37 | if err != nil { 38 | color.Red("Error getting connections: %v", err) 39 | return &openRootModel{} 40 | } 41 | 42 | items := make([]table.Row, len(connections)) 43 | for i, conn := range connections { 44 | items[i] = table.Row{conn.Name, conn.Type.GetMainType(), conn.Type.GetSubtype()} 45 | } 46 | 47 | columns := []table.Column{ 48 | {Title: "Name", Width: 25}, 49 | {Title: "Type", Width: 15}, 50 | {Title: "Subtype", Width: 20}, 51 | } 52 | 53 | t := table.New( 54 | table.WithColumns(columns), 55 | table.WithRows(items), 56 | table.WithFocused(true), 57 | table.WithHeight(10), 58 | ) 59 | 60 | s := table.DefaultStyles() 61 | s.Header = s.Header. 62 | BorderStyle(lipgloss.NormalBorder()). 63 | BorderForeground(lipgloss.Color("240")). 64 | BorderBottom(true). 65 | Bold(false) 66 | s.Selected = s.Selected. 67 | Foreground(lipgloss.Color("0")). 68 | Background(lipgloss.Color("212")). 69 | Bold(true) 70 | t.SetStyles(s) 71 | 72 | onSelect := func(selected string) tea.Msg { 73 | conn, err := config.GetConnectionByName(selected) 74 | if err != nil { 75 | return fmt.Errorf("error getting connection %s: %w", selected, err) 76 | } 77 | 78 | return ui.TransitionToShellMsg{ 79 | Connection: conn, 80 | } 81 | } 82 | 83 | tblModel := ui.NewTableModel(t, onSelect, false) 84 | 85 | return &openRootModel{ 86 | step: stepPick, 87 | tableModel: tblModel, 88 | } 89 | } 90 | 91 | // Init initializes the openRootModel. 92 | func (m *openRootModel) Init() tea.Cmd { 93 | return m.tableModel.Init() 94 | } 95 | 96 | // Update handles messages and updates the model state. 97 | func (m *openRootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 98 | switch msg := msg.(type) { 99 | 100 | case ui.TransitionToShellMsg: 101 | m.shellModel = shell.NewShellModel(msg.Connection) 102 | m.step = stepShell 103 | return m.shellModel, m.shellModel.Init() 104 | 105 | case error: 106 | color.Red("Error: %v", msg) 107 | return m, nil 108 | } 109 | 110 | if m.step == stepPick { 111 | var cmd tea.Cmd 112 | updatedTable, cmd := m.tableModel.Update(msg) 113 | m.tableModel = updatedTable.(tableModel) 114 | return m, cmd 115 | } 116 | 117 | if m.step == stepShell && m.shellModel != nil { 118 | var cmd tea.Cmd 119 | m.shellModel, cmd = m.shellModel.Update(msg) 120 | return m, cmd 121 | } 122 | 123 | return m, nil 124 | } 125 | 126 | // View renders the current view based on the model state. 127 | func (m *openRootModel) View() string { 128 | if m.step == stepPick { 129 | return m.tableModel.View() 130 | } 131 | 132 | if m.step == stepShell && m.shellModel != nil { 133 | return m.shellModel.View() 134 | } 135 | 136 | return "No view" 137 | } 138 | -------------------------------------------------------------------------------- /pkg/ui/doc.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | // This package provides UI components and utilities. 4 | -------------------------------------------------------------------------------- /pkg/ui/shell/actions.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import tea "github.com/charmbracelet/bubbletea" 4 | 5 | func (m shellModel) runInitialChecks() tea.Msg { 6 | err := m.popsConnection.CheckAuthentication() 7 | if err != nil { 8 | return errMsg{err} 9 | } 10 | 11 | err = m.popsConnection.SetContext() 12 | if err != nil { 13 | return errMsg{err} 14 | } 15 | 16 | return checkPassedMsg{} 17 | } 18 | 19 | func (m shellModel) generateCommand(prompt string) tea.Cmd { 20 | return func() tea.Msg { 21 | cmd, err := m.popsConnection.GetCommand(prompt) 22 | if err != nil { 23 | return errMsg{err} 24 | } 25 | 26 | return commandMsg{ 27 | command: cmd, 28 | } 29 | } 30 | } 31 | 32 | func (m shellModel) runCommand(command string) tea.Cmd { 33 | return func() tea.Msg { 34 | out, err := m.popsConnection.ExecuteCommand(command) 35 | if err != nil { 36 | return errMsg{err} 37 | } 38 | 39 | outStr, err := m.popsConnection.FormatResultAsTable(out) 40 | if err != nil { 41 | return errMsg{err} 42 | } 43 | 44 | return outputMsg{ 45 | output: outStr, 46 | } 47 | } 48 | } 49 | 50 | func (m shellModel) generateAnswer(prompt string) tea.Cmd { 51 | return func() tea.Msg { 52 | answer, err := m.popsConnection.GetAnswer(prompt) 53 | if err != nil { 54 | return errMsg{err} 55 | } 56 | 57 | return answerMsg{ 58 | answer, 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/ui/shell/shell.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/charmbracelet/bubbles/spinner" 7 | "github.com/charmbracelet/bubbles/textinput" 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/charmbracelet/lipgloss" 10 | "github.com/prompt-ops/pops/pkg/conn" 11 | "golang.org/x/term" 12 | ) 13 | 14 | type queryMode int 15 | 16 | const ( 17 | modeCommand queryMode = iota 18 | modeAnswer 19 | ) 20 | 21 | const ( 22 | stepInitialChecks = iota 23 | stepShowContext 24 | stepEnterPrompt 25 | stepGenerateCommand 26 | stepGetAnswer 27 | stepConfirmRun 28 | stepRunCommand 29 | stepDone 30 | ) 31 | 32 | // historyEntry stores a single cycle of user prompt and output 33 | type historyEntry struct { 34 | prompt string 35 | cmd string 36 | 37 | // Command or Answer 38 | mode string 39 | 40 | output string 41 | err error 42 | } 43 | 44 | type shellModel struct { 45 | step int 46 | promptInput textinput.Model 47 | command string 48 | confirmInput textinput.Model 49 | output string 50 | err error 51 | history []historyEntry 52 | historyIndex int 53 | connection conn.Connection 54 | popsConnection conn.ConnectionInterface 55 | spinner spinner.Model 56 | checkPassed bool 57 | mode queryMode 58 | windowWidth int 59 | } 60 | 61 | func NewShellModel(connection conn.Connection) shellModel { 62 | ti := textinput.New() 63 | ti.Placeholder = "Define the command or query to be generated via Prompt-Ops..." 64 | ti.Focus() 65 | ti.CharLimit = 512 66 | ti.Width = 100 67 | 68 | ci := textinput.New() 69 | ci.Placeholder = "Y/n" 70 | ci.CharLimit = 3 71 | ci.Width = 100 72 | ci.PromptStyle.Padding(0, 1) 73 | 74 | sp := spinner.New() 75 | sp.Spinner = spinner.Dot 76 | 77 | // Get the right connection implementation 78 | popsConn, err := conn.GetConnection(connection) 79 | if err != nil { 80 | panic(err) 81 | } 82 | 83 | return shellModel{ 84 | step: stepInitialChecks, 85 | promptInput: ti, 86 | confirmInput: ci, 87 | history: []historyEntry{}, 88 | connection: connection, 89 | popsConnection: popsConn, 90 | spinner: sp, 91 | mode: modeCommand, 92 | } 93 | } 94 | 95 | func (m shellModel) Init() tea.Cmd { 96 | return tea.Batch( 97 | m.spinner.Tick, 98 | m.runInitialChecks, 99 | tea.EnterAltScreen, 100 | requestWindowSize(), 101 | ) 102 | } 103 | 104 | func (m shellModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 105 | if key, ok := msg.(tea.KeyMsg); ok && key.Type == tea.KeyCtrlC { 106 | return m, tea.Quit 107 | } 108 | 109 | switch msg := msg.(type) { 110 | case tea.WindowSizeMsg: 111 | m.windowWidth = msg.Width 112 | return m, nil 113 | 114 | case checkPassedMsg: 115 | m.checkPassed = true 116 | m.step = stepEnterPrompt 117 | return m, textinput.Blink 118 | 119 | case errMsg: 120 | m.err = msg.err 121 | m.step = stepDone 122 | return m, nil 123 | } 124 | 125 | switch m.step { 126 | case stepInitialChecks: 127 | var cmd tea.Cmd 128 | m.spinner, cmd = m.spinner.Update(msg) 129 | return m, cmd 130 | 131 | case stepShowContext: 132 | switch msg := msg.(type) { 133 | case tea.KeyMsg: 134 | if msg.Type == tea.KeyF1 { 135 | m.step = stepEnterPrompt 136 | } 137 | } 138 | return m, nil 139 | 140 | case stepEnterPrompt: 141 | var cmd tea.Cmd 142 | m.promptInput, cmd = m.promptInput.Update(msg) 143 | 144 | switch msg := msg.(type) { 145 | case tea.KeyMsg: 146 | switch msg.Type { 147 | case tea.KeyUp: 148 | if m.historyIndex > 0 && len(m.history) > 0 { 149 | m.historyIndex-- 150 | previousPrompt := m.history[m.historyIndex].prompt 151 | m.promptInput.SetValue(previousPrompt) 152 | m.promptInput.CursorEnd() 153 | } 154 | case tea.KeyDown: 155 | if m.historyIndex < len(m.history)-1 { 156 | m.historyIndex++ 157 | nextPrompt := m.history[m.historyIndex].prompt 158 | m.promptInput.SetValue(nextPrompt) 159 | m.promptInput.CursorEnd() 160 | } else { 161 | m.historyIndex = len(m.history) 162 | m.promptInput.SetValue("") 163 | } 164 | 165 | case tea.KeyLeft, tea.KeyRight: 166 | if m.mode == modeCommand { 167 | m.mode = modeAnswer 168 | } else { 169 | m.mode = modeCommand 170 | } 171 | m.updatePromptInputPlaceholder() 172 | 173 | case tea.KeyEnter: 174 | prompt := strings.TrimSpace(m.promptInput.Value()) 175 | if prompt != "" { 176 | if m.mode == modeCommand { 177 | m.step = stepGenerateCommand 178 | return m, m.generateCommand(prompt) 179 | } else { 180 | m.step = stepGetAnswer 181 | return m, m.generateAnswer(prompt) 182 | } 183 | } 184 | 185 | case tea.KeyCtrlC, tea.KeyEsc: 186 | return m, tea.Quit 187 | 188 | case tea.KeyF1: 189 | m.step = stepShowContext 190 | output, err := m.popsConnection.GetFormattedContext() 191 | if err != nil { 192 | m.err = err 193 | return m, nil 194 | } 195 | m.output = output 196 | return m, nil 197 | } 198 | } 199 | return m, cmd 200 | 201 | case stepGenerateCommand: 202 | if cmdMsg, ok := msg.(commandMsg); ok { 203 | m.command = cmdMsg.command 204 | m.step = stepConfirmRun 205 | m.confirmInput.Focus() 206 | return m, textinput.Blink 207 | } 208 | return m, nil 209 | 210 | case stepGetAnswer: 211 | if ansMsg, ok := msg.(answerMsg); ok { 212 | m.output = ansMsg.answer 213 | m.step = stepDone 214 | return m, nil 215 | } 216 | return m, nil 217 | 218 | case stepConfirmRun: 219 | var cmd tea.Cmd 220 | m.confirmInput, cmd = m.confirmInput.Update(msg) 221 | if key, ok := msg.(tea.KeyMsg); ok && key.Type == tea.KeyEnter { 222 | val := m.confirmInput.Value() 223 | if val == "Y" || val == "y" { 224 | m.step = stepRunCommand 225 | return m, m.runCommand(m.command) 226 | } else if val == "N" || val == "n" { 227 | m.step = stepEnterPrompt 228 | m.promptInput.Reset() 229 | m.confirmInput.Reset() 230 | m.historyIndex = len(m.history) 231 | return m, textinput.Blink 232 | } 233 | } 234 | return m, cmd 235 | 236 | case stepRunCommand: 237 | if outMsg, ok := msg.(outputMsg); ok { 238 | m.output = outMsg.output 239 | m.step = stepDone 240 | return m, nil 241 | } 242 | return m, nil 243 | 244 | case stepDone: 245 | if m.err != nil { 246 | if key, ok := msg.(tea.KeyMsg); ok { 247 | switch key.String() { 248 | case "q", "esc", "ctrl+c": 249 | return m, tea.Quit 250 | case "enter": 251 | m.err = nil 252 | m.step = stepEnterPrompt 253 | m.promptInput.Reset() 254 | return m, textinput.Blink 255 | } 256 | } 257 | return m, nil 258 | } 259 | 260 | if key, ok := msg.(tea.KeyMsg); ok { 261 | switch key.String() { 262 | case "q", "esc", "ctrl+c": 263 | return m, tea.Quit 264 | case "enter": 265 | mode := "Command" 266 | if m.mode == modeAnswer { 267 | mode = "Answer" 268 | } 269 | 270 | m.history = append(m.history, historyEntry{ 271 | prompt: m.promptInput.Value(), 272 | cmd: m.command, 273 | mode: mode, 274 | output: m.output, 275 | err: m.err, 276 | }) 277 | 278 | m.historyIndex = len(m.history) 279 | m.step = stepEnterPrompt 280 | m.promptInput.Reset() 281 | m.confirmInput.Reset() 282 | return m, textinput.Blink 283 | } 284 | } 285 | 286 | return m, nil 287 | 288 | default: 289 | return m, tea.Quit 290 | } 291 | } 292 | 293 | func (m shellModel) View() string { 294 | historyView := lipgloss.NewStyle(). 295 | MaxWidth(m.windowWidth-2). 296 | Margin(0, 1). 297 | Render(m.viewHistory()) 298 | 299 | var content string 300 | 301 | switch m.step { 302 | case stepInitialChecks: 303 | content = m.viewInitialChecks() 304 | 305 | case stepShowContext: 306 | content = m.viewShowContext() 307 | 308 | case stepEnterPrompt: 309 | content = m.viewEnterPrompt() 310 | 311 | case stepGenerateCommand: 312 | content = m.viewGenerateCommand() 313 | 314 | case stepGetAnswer: 315 | content = m.viewGetAnswer() 316 | 317 | case stepConfirmRun: 318 | content = m.viewConfirmRun() 319 | 320 | case stepRunCommand: 321 | content = m.viewRunCommand() 322 | 323 | case stepDone: 324 | content = m.viewDone() 325 | 326 | default: 327 | content = "" 328 | } 329 | 330 | return lipgloss.JoinVertical(lipgloss.Top, historyView, content) 331 | } 332 | 333 | func requestWindowSize() tea.Cmd { 334 | return func() tea.Msg { 335 | w, h, err := term.GetSize(0) 336 | if err != nil { 337 | w, h = 80, 24 338 | } 339 | return tea.WindowSizeMsg{ 340 | Width: w, 341 | Height: h, 342 | } 343 | } 344 | } 345 | 346 | func (m *shellModel) updatePromptInputPlaceholder() { 347 | if m.mode == modeAnswer { 348 | m.promptInput.Placeholder = "Ask a question via Prompt-Ops..." 349 | } else { 350 | m.promptInput.Placeholder = "Define the command or query to be generated via Prompt-Ops..." 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /pkg/ui/shell/styles.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import "github.com/charmbracelet/lipgloss" 4 | 5 | var ( 6 | titleStyle = lipgloss.NewStyle(). 7 | Bold(true). 8 | Foreground(lipgloss.Color("15")). 9 | Padding(0, 1) 10 | 11 | promptStyle = lipgloss.NewStyle(). 12 | Foreground(lipgloss.Color("10")). 13 | Padding(0, 1) 14 | 15 | commandConfirmationTitleStyle = lipgloss.NewStyle(). 16 | Bold(true). 17 | Foreground(lipgloss.Color("15")). 18 | Padding(0, 1) 19 | 20 | commandConfirmationContentStyle = lipgloss.NewStyle(). 21 | Foreground(lipgloss.Color("10")). 22 | Padding(0, 1) 23 | 24 | commandConfirmationResponseStyle = lipgloss.NewStyle(). 25 | Foreground(lipgloss.Color("10")). 26 | Padding(0, 1) 27 | 28 | outputStyle = lipgloss.NewStyle(). 29 | Foreground(lipgloss.Color("10")). 30 | Padding(0, 1) 31 | 32 | errorStyle = lipgloss.NewStyle(). 33 | Foreground(lipgloss.Color("10")). 34 | Padding(0, 1) 35 | 36 | footerStyle = lipgloss.NewStyle(). 37 | Bold(true). 38 | Foreground(lipgloss.Color("10")). 39 | Padding(0, 1) 40 | 41 | // History related styles 42 | historyContainerStyle = lipgloss.NewStyle(). 43 | Border(lipgloss.RoundedBorder()). 44 | BorderForeground(lipgloss.Color("240")). 45 | Padding(0, 1). 46 | Margin(1, 0) 47 | 48 | historyLabelStyle = lipgloss.NewStyle(). 49 | Bold(true). 50 | Foreground(lipgloss.Color("212")) 51 | 52 | historyCommandStyle = lipgloss.NewStyle(). 53 | Foreground(lipgloss.Color("10")) 54 | ) 55 | -------------------------------------------------------------------------------- /pkg/ui/shell/types.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | type commandMsg struct { 4 | command string 5 | } 6 | 7 | type outputMsg struct { 8 | output string 9 | } 10 | 11 | type answerMsg struct { 12 | answer string 13 | } 14 | 15 | type checkPassedMsg struct { 16 | } 17 | 18 | type errMsg struct { 19 | err error 20 | } 21 | -------------------------------------------------------------------------------- /pkg/ui/shell/views.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/lipgloss" 7 | ) 8 | 9 | func (m shellModel) renderFooter(text string) string { 10 | return footerStyle.Render(text) 11 | } 12 | 13 | func (m shellModel) viewInitialChecks() string { 14 | if m.checkPassed { 15 | return outputStyle.Render("✅ Authentication passed!\n\n") 16 | } 17 | return fmt.Sprintf( 18 | "%s %s", 19 | m.spinner.View(), 20 | titleStyle.Render("Checking Authentication..."), 21 | ) 22 | } 23 | 24 | func (m shellModel) viewEnterPrompt() string { 25 | var title string 26 | var modeStr string 27 | 28 | if m.mode == modeCommand { 29 | title = "🤖 Request a command/query" 30 | modeStr = "command/query" 31 | } else { 32 | title = "💡 Ask a question" 33 | modeStr = "answer" 34 | } 35 | 36 | footer := m.renderFooter("Use ←/→ to switch between modes (currently " + modeStr + "). Press Enter when ready.\n\nPress F1 to show context.") 37 | 38 | return fmt.Sprintf( 39 | "%s\n\n%s\n\n%s", 40 | titleStyle.Render(title), 41 | promptStyle.Render(m.promptInput.View()), 42 | footer, 43 | ) 44 | } 45 | 46 | func (m shellModel) viewShowContext() string { 47 | footer := m.renderFooter("Press F1 to return to prompt.") 48 | 49 | return fmt.Sprintf( 50 | "%s\n\n%s", 51 | titleStyle.Render("ℹ️ Current Context"), 52 | outputStyle.Render(m.output), 53 | ) + "\n\n" + footer 54 | } 55 | 56 | func (m shellModel) viewGenerateCommand() string { 57 | return titleStyle.Render("🤖 Generating command...") 58 | } 59 | 60 | func (m shellModel) viewGetAnswer() string { 61 | return titleStyle.Render("🤔 Getting your answer...") 62 | } 63 | 64 | func (m shellModel) viewConfirmRun() string { 65 | return fmt.Sprintf( 66 | "%s\n\n%s\n\n%s", 67 | commandConfirmationTitleStyle.Render("🚀 Would you like to run the following command? (Y/n)"), 68 | commandConfirmationContentStyle.Render("🐳 "+m.command), 69 | commandConfirmationResponseStyle.Render(m.confirmInput.View()), 70 | ) 71 | } 72 | 73 | func (m shellModel) viewRunCommand() string { 74 | return titleStyle.Render("🏃 Running command...") 75 | } 76 | 77 | func (m shellModel) viewDone() string { 78 | width := m.calculateShareViewWidth() 79 | 80 | outStyle := lipgloss.NewStyle(). 81 | Width(width). 82 | MaxWidth(width) 83 | 84 | var content string 85 | if m.err != nil { 86 | content = fmt.Sprintf("%v\n", m.err) 87 | content = errorStyle.Render(content) 88 | } else { 89 | content = fmt.Sprintf("%s\n", m.output) 90 | content = outputStyle.Render(content) 91 | } 92 | 93 | content = outStyle.Render(content) 94 | footer := m.renderFooter("Press 'q' or 'esc' or Ctrl+C to quit, or enter a new prompt.") 95 | return lipgloss.JoinVertical(lipgloss.Top, content, footer) 96 | } 97 | 98 | func (m shellModel) viewHistory() string { 99 | if len(m.history) == 0 { 100 | return "" 101 | } 102 | 103 | var entries []string 104 | for _, h := range m.history { 105 | var promptLine string 106 | var modeLine string 107 | if h.mode == "Command" { 108 | promptLine = lipgloss.JoinHorizontal( 109 | lipgloss.Top, 110 | historyLabelStyle.Render("Prompt: "), 111 | promptStyle.Render(h.prompt), 112 | ) 113 | 114 | modeLine = lipgloss.JoinHorizontal( 115 | lipgloss.Top, 116 | historyLabelStyle.Render("Command: "), 117 | historyCommandStyle.Render(h.cmd), 118 | ) 119 | } else { 120 | promptLine = lipgloss.JoinHorizontal( 121 | lipgloss.Top, 122 | historyLabelStyle.Render("Question: "), 123 | promptStyle.Render(h.prompt), 124 | ) 125 | 126 | modeLine = lipgloss.JoinHorizontal( 127 | lipgloss.Top, 128 | historyLabelStyle.Render("Answer: "), 129 | ) 130 | } 131 | 132 | outputLine := lipgloss.JoinHorizontal( 133 | lipgloss.Top, 134 | outputStyle.Render(h.output), 135 | ) 136 | 137 | content := lipgloss.JoinVertical( 138 | lipgloss.Left, 139 | promptLine, 140 | modeLine, 141 | outputLine, 142 | ) 143 | 144 | content = lipgloss.JoinVertical(lipgloss.Left, content) 145 | 146 | // Adding minus 10 to the history box width. 147 | // FIXME: Right border is not visible. 148 | width := m.calculateShareViewWidth() 149 | boxed := historyContainerStyle. 150 | Width(width). 151 | MaxWidth(width). 152 | Render(content) 153 | entries = append(entries, boxed) 154 | } 155 | 156 | return lipgloss.JoinVertical(lipgloss.Left, entries...) 157 | } 158 | 159 | func (m shellModel) calculateShareViewWidth() int { 160 | maxWidth := m.windowWidth - 2 161 | if maxWidth < 20 { 162 | maxWidth = 20 163 | } 164 | return maxWidth 165 | } 166 | -------------------------------------------------------------------------------- /pkg/ui/spinner.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/charmbracelet/bubbles/spinner" 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/fatih/color" 10 | ) 11 | 12 | type spinnerMsg struct{} 13 | 14 | type spinnerModel struct { 15 | spinner spinner.Model 16 | msg string 17 | } 18 | 19 | func newSpinnerModel(msg string) spinnerModel { 20 | s := spinner.New() 21 | s.Spinner = spinner.Dot 22 | return spinnerModel{ 23 | spinner: s, 24 | msg: msg, 25 | } 26 | } 27 | 28 | func (m spinnerModel) Init() tea.Cmd { 29 | return m.spinner.Tick 30 | } 31 | 32 | func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 33 | switch msg := msg.(type) { 34 | case spinner.TickMsg: 35 | sp, cmd := m.spinner.Update(msg) 36 | m.spinner = sp 37 | return m, cmd 38 | case spinnerMsg: 39 | return m, tea.Quit 40 | } 41 | return m, nil 42 | } 43 | 44 | func (m spinnerModel) View() string { 45 | return fmt.Sprintf("%s %s", m.spinner.View(), m.msg) 46 | } 47 | 48 | // RunWithSpinner runs a Bubble Tea program with a spinner and executes the provided function. 49 | func RunWithSpinner(msg string, fn func() error) error { 50 | model := newSpinnerModel(msg) 51 | p := tea.NewProgram(model) 52 | 53 | // Run the spinner in a separate goroutine 54 | go func() { 55 | time.Sleep(2 * time.Second) 56 | 57 | err := fn() 58 | if err != nil { 59 | color.Red("Error: %v", err) 60 | } else { 61 | color.Green("Success") 62 | } 63 | 64 | p.Send(spinnerMsg{}) 65 | }() 66 | 67 | // Start the spinner program 68 | if _, err := p.Run(); err != nil { 69 | return err 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/ui/table.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/table" 5 | tea "github.com/charmbracelet/bubbletea" 6 | "github.com/charmbracelet/lipgloss" 7 | ) 8 | 9 | var baseStyle = lipgloss.NewStyle(). 10 | BorderStyle(lipgloss.RoundedBorder()). 11 | BorderForeground(lipgloss.Color("240")) 12 | 13 | type tableModel struct { 14 | table table.Model 15 | selected string 16 | isListOnly bool 17 | 18 | // onSelect is an optional function that will be called 19 | // when a row is selected if specified. 20 | onSelect func(string) tea.Msg 21 | } 22 | 23 | func NewTableModel(table table.Model, onSelect func(string) tea.Msg, isListOnly bool) *tableModel { 24 | return &tableModel{ 25 | table: table, 26 | onSelect: onSelect, 27 | isListOnly: isListOnly, 28 | } 29 | } 30 | 31 | func (m *tableModel) Init() tea.Cmd { 32 | return nil 33 | } 34 | 35 | func (m *tableModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 36 | if m.isListOnly { 37 | // If the table is just a list, ignore key events for selection 38 | switch msg := msg.(type) { 39 | case tea.KeyMsg: 40 | switch msg.String() { 41 | case "q", "ctrl+c": 42 | return m, tea.Quit 43 | } 44 | } 45 | } else { 46 | switch msg := msg.(type) { 47 | case tea.KeyMsg: 48 | switch msg.String() { 49 | case "esc": 50 | if m.table.Focused() { 51 | m.table.Blur() 52 | } else { 53 | m.table.Focus() 54 | } 55 | case "q", "ctrl+c": 56 | return m, tea.Quit 57 | case "enter": 58 | selectedRow := m.table.SelectedRow() 59 | if selectedRow == nil { 60 | // No selection made 61 | return m, tea.Quit 62 | } 63 | m.selected = selectedRow[0] 64 | 65 | // If onSelect is specified, call it. 66 | if m.onSelect != nil { 67 | return m, func() tea.Msg { 68 | return m.onSelect(m.selected) 69 | } 70 | } 71 | 72 | return m, tea.Quit 73 | } 74 | } 75 | } 76 | 77 | m.table, _ = m.table.Update(msg) 78 | return m, nil 79 | } 80 | 81 | func (m *tableModel) View() string { 82 | return baseStyle.Render(m.table.View()) + "\n" 83 | } 84 | 85 | func (m *tableModel) Selected() string { 86 | return m.selected 87 | } 88 | -------------------------------------------------------------------------------- /pkg/ui/types.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/prompt-ops/pops/pkg/conn" 5 | ) 6 | 7 | type TransitionToShellMsg struct { 8 | Connection conn.Connection 9 | } 10 | 11 | type TransitionToCreateMsg struct { 12 | ConnectionType string 13 | } 14 | 15 | var ( 16 | // EnterConnectionNameMessage is the message displayed when the user is prompted to enter a connection name. 17 | EnterConnectionNameMessage = "Enter connection name:" 18 | 19 | // QuitMessage is the message displayed to show user how to quit the application. 20 | QuitMessage = "Press 'q' or 'esc' or Ctrl+C to quit." 21 | ) 22 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # Get the latest release version if not provided 6 | VERSION=${1:-$(curl -s https://api.github.com/repos/prompt-ops/pops/releases/latest | grep '"tag_name"' | cut -d '"' -f 4)} 7 | 8 | # Check if the version is valid 9 | if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 10 | echo "Error: Invalid version format: '$VERSION'. Expected format: vX.Y.Z" 11 | exit 1 12 | fi 13 | 14 | echo "Installing Prompt-Ops $VERSION..." 15 | 16 | # Detect OS and ARCH 17 | OS=$(uname | tr '[:upper:]' '[:lower:]') 18 | ARCH=$(uname -m) 19 | 20 | case "$ARCH" in 21 | x86_64) ARCH="amd64" ;; 22 | arm64 | aarch64) ARCH="arm64" ;; 23 | *) 24 | echo "Error: Unsupported architecture: $ARCH" 25 | exit 1 26 | ;; 27 | esac 28 | 29 | echo "Detected OS: $OS" 30 | echo "Detected ARCH: $ARCH" 31 | 32 | # Construct download URL 33 | URL="https://github.com/prompt-ops/pops/releases/download/$VERSION/pops-${OS}-${ARCH}" 34 | 35 | # Download binary 36 | echo "Downloading Prompt-Ops $VERSION from $URL..." 37 | curl -Lo pops "$URL" 38 | chmod +x pops 39 | 40 | # Move to /usr/local/bin (requires sudo) 41 | echo "Installing Prompt-Ops to /usr/local/bin..." 42 | sudo mv pops /usr/local/bin/ 43 | 44 | # Verify installation 45 | if command -v pops >/dev/null 2>&1; then 46 | echo "Prompt-Ops $VERSION installed successfully!" 47 | pops version 48 | else 49 | echo "Error: Installation failed. 'pops' command not found." 50 | exit 1 51 | fi 52 | --------------------------------------------------------------------------------