From e30fec2f607d1ef7efb47a2ae73cc2960b47e789 Mon Sep 17 00:00:00 2001 From: ytimocin Date: Sat, 4 Jan 2025 12:56:05 -0800 Subject: [PATCH] Cleaning up the code, adding lint and build workflows, adding makefiles for build, lint, and test. Signed-off-by: ytimocin --- .github/scripts/install.sh | 2 +- .github/workflows/build.yml | 61 +++++++++++ .github/workflows/release.yml | 20 ++-- .gitignore | 3 + Makefile | 2 + ai/ai.go | 3 +- cmd/connection/cloud/create.go | 4 +- cmd/connection/cloud/open.go | 4 +- cmd/connection/create.go | 4 +- cmd/connection/db/create.go | 4 +- cmd/connection/db/open.go | 4 +- cmd/connection/kubernetes/ai.go | 164 ---------------------------- cmd/connection/kubernetes/create.go | 4 +- cmd/connection/kubernetes/open.go | 4 +- make/build.mk | 17 +++ make/install.mk | 9 ++ make/lint.mk | 13 +++ ui/db/create.go | 3 +- ui/kubernetes/create.go | 1 - ui/kubernetes/open.go | 2 +- 20 files changed, 134 insertions(+), 194 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 Makefile delete mode 100644 cmd/connection/kubernetes/ai.go create mode 100644 make/build.mk create mode 100644 make/install.mk create mode 100644 make/lint.mk diff --git a/.github/scripts/install.sh b/.github/scripts/install.sh index 9d8c6fc..0ad6f18 100755 --- a/.github/scripts/install.sh +++ b/.github/scripts/install.sh @@ -19,7 +19,7 @@ ARCH=$(uname -m) if [[ "$ARCH" == "x86_64" ]]; then ARCH="amd64" -elif [[ "$ARCH" == "aarch64" ]]; then +elif [[ "$ARCH" == "arm64" || "$ARCH" == "aarch64" ]]; then ARCH="arm64" else echo "Unsupported architecture: $ARCH" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..31d9f25 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,61 @@ +name: Build + +on: + push: + branches: + - main + - release/* + tags: + - "v*.*.*" + pull_request: + branches: + - main + - release/* + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + goos: [linux, darwin, windows] + goarch: [amd64, arm64] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.22 + cache: false + + - name: Install dependencies + run: go mod tidy + + - name: Run tests + run: go test -v ./... + + - name: Build binaries + run: | + echo "Building for GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }}" + GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \ + make build + + - name: Rename Windows binaries with .exe + if: matrix.goos == 'windows' + run: mv dist/pops-windows-${{ matrix.goarch }} dist/pops-windows-${{ matrix.goarch }}.exe + + - name: Validate binary + run: | + file dist/pops-${{ matrix.goos }}-${{ matrix.goarch }}* + + - name: Upload binaries as artifact + uses: actions/upload-artifact@v4 + with: + name: pops-${{ matrix.goos }}-${{ matrix.goarch }} + path: dist/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c7beb5..06e7571 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,14 +30,14 @@ jobs: - name: Install dependencies run: go mod tidy - - name: Create dist directory - run: mkdir -p dist + - name: Run tests + run: go test -v ./... - name: Build binaries run: | echo "Building for GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }}" - GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} CGO_ENABLED=0 GO111MODULE=on \ - go build -ldflags="-s -w" -o dist/pops-${{ matrix.goos }}-${{ matrix.goarch }} + GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \ + make build - name: Rename Windows binaries with .exe if: matrix.goos == 'windows' @@ -50,8 +50,8 @@ jobs: - name: Upload binaries as artifact uses: actions/upload-artifact@v4 with: - name: pops-${{ matrix.goos }}-${{ matrix.goarch }} - path: dist/ + name: pops-binaries + path: dist/* release: runs-on: ubuntu-latest @@ -64,11 +64,11 @@ jobs: - name: Download build artifacts uses: actions/download-artifact@v4 with: - path: release + path: dist/ pattern: pops-* - - name: List release folder - run: ls -R release + - name: List dist folder + run: ls -R dist/ - name: Create GitHub Release id: create_release @@ -77,6 +77,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: repository: prompt-ops/pops - files: release/* + files: dist/* draft: false prerelease: false diff --git a/.gitignore b/.gitignore index 24ab6a4..4e39d9a 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ Thumbs.db # `pops` binary pops + +# dist directory +dist/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..204ec02 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +# Include all make files in the make directory +include $(wildcard make/*.mk) \ No newline at end of file diff --git a/ai/ai.go b/ai/ai.go index d9b3de6..efaefa6 100644 --- a/ai/ai.go +++ b/ai/ai.go @@ -108,10 +108,11 @@ func parseResponse(response string) (ParsedResponse, error) { // parseSuggestions extracts the suggestions from the response. func parseSuggestions(lines []string) []string { var suggestions []string + re := regexp.MustCompile(`^\d+\.\s+`) for _, line := range lines { line = strings.TrimSpace(line) // Match numbered suggestions (e.g., "1. Describe one of the pods") - if matched, _ := regexp.MatchString(`^\d+\.\s+`, line); matched { + if matched := re.MatchString(line); matched { suggestions = append(suggestions, line) } } diff --git a/cmd/connection/cloud/create.go b/cmd/connection/cloud/create.go index 999d03e..ba41945 100644 --- a/cmd/connection/cloud/create.go +++ b/cmd/connection/cloud/create.go @@ -27,9 +27,9 @@ func (m *createModel) Init() tea.Cmd { } func (m *createModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg.(type) { + switch msg := msg.(type) { case ui.TransitionToShellMsg: - shellModel := ui.NewShellModel(msg.(ui.TransitionToShellMsg).Connection) + shellModel := ui.NewShellModel(msg.Connection) m.current = shellModel return m, shellModel.Init() } diff --git a/cmd/connection/cloud/open.go b/cmd/connection/cloud/open.go index e579046..b575c6b 100644 --- a/cmd/connection/cloud/open.go +++ b/cmd/connection/cloud/open.go @@ -28,9 +28,9 @@ func (m *openModel) Init() tea.Cmd { } func (m *openModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg.(type) { + switch msg := msg.(type) { case ui.TransitionToShellMsg: - m.current = ui.NewShellModel(msg.(ui.TransitionToShellMsg).Connection) + m.current = ui.NewShellModel(msg.Connection) return m, m.current.Init() } var cmd tea.Cmd diff --git a/cmd/connection/create.go b/cmd/connection/create.go index c3a5818..aaef428 100644 --- a/cmd/connection/create.go +++ b/cmd/connection/create.go @@ -104,10 +104,10 @@ func (m *createModel) Init() tea.Cmd { func (m *createModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.currentStep { case createStepTypeSelection: - switch msg.(type) { + switch msg := msg.(type) { case ui.TransitionToCreateMsg: fmt.Println("Transitioning to create") - connectionType := msg.(ui.TransitionToCreateMsg).ConnectionType + connectionType := msg.ConnectionType createModel, err := factory.GetCreateModel(connectionType) if err != nil { return m, tea.Quit diff --git a/cmd/connection/db/create.go b/cmd/connection/db/create.go index d7cd614..39416af 100644 --- a/cmd/connection/db/create.go +++ b/cmd/connection/db/create.go @@ -27,9 +27,9 @@ func (m *createModel) Init() tea.Cmd { } func (m *createModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg.(type) { + switch msg := msg.(type) { case ui.TransitionToShellMsg: - shellModel := ui.NewShellModel(msg.(ui.TransitionToShellMsg).Connection) + shellModel := ui.NewShellModel(msg.Connection) m.current = shellModel return m, shellModel.Init() } diff --git a/cmd/connection/db/open.go b/cmd/connection/db/open.go index 925e9ce..6864ded 100644 --- a/cmd/connection/db/open.go +++ b/cmd/connection/db/open.go @@ -27,9 +27,9 @@ func (m *openModel) Init() tea.Cmd { } func (m *openModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg.(type) { + switch msg := msg.(type) { case ui.TransitionToShellMsg: - m.current = ui.NewShellModel(msg.(ui.TransitionToShellMsg).Connection) + m.current = ui.NewShellModel(msg.Connection) return m, m.current.Init() } var cmd tea.Cmd diff --git a/cmd/connection/kubernetes/ai.go b/cmd/connection/kubernetes/ai.go deleted file mode 100644 index 30a26d0..0000000 --- a/cmd/connection/kubernetes/ai.go +++ /dev/null @@ -1,164 +0,0 @@ -package kubernetes - -import ( - "context" - "fmt" - "os" - "regexp" - "strings" - - "github.com/joho/godotenv" - "github.com/openai/openai-go" - "github.com/openai/openai-go/option" -) - -type CommandType string - -const ( - KubernetesCommand CommandType = "kubectl command" - RDBMSQuery CommandType = "PostgreSQL SQL query" - CloudCommand CommandType = "Azure `az` command" -) - -var ( - // defaultSystemMessage is the system message that is sent to the OpenAI API to help it understand the context of the user's input. - defaultSystemMessage = `You are a helpful assistant that translates natural language commands to %s. - This is how your response structure MUST be like: - - Command: kubectl get pods - Suggested next steps: - 1. Describe one of the pods. - 2. View logs of one of the pods. - - Command: SELECT * FROM table_name; - Suggested next steps: - 1. Filter the results based on a specific condition. - 2. Join this table with another table. - - Command: az vm list - Suggested next steps: - 1. Start a specific VM. - 2. Stop a specific VM. - - Do not include any Markdown-type formatting. Only provide plain text.` - - // defaultUserMessage is the user message that is sent to the OpenAI API to help it understand the context of the user's input. - defaultUserMessage = "User prompt: %s. Additional context: %s" -) - -// ParsedResponse holds the parsed command and suggested next steps. -type ParsedResponse struct { - Command string - SuggestedSteps []string -} - -func getCommand(input string, commandType CommandType, extraContext string) (ParsedResponse, error) { - err := godotenv.Load(".env.local") - if err != nil { - return ParsedResponse{}, fmt.Errorf("Error loading .env.local file: %v", err) - } - - apiKey := os.Getenv("OPENAI_API_KEY") - if apiKey == "" { - return ParsedResponse{}, fmt.Errorf("OpenAI API key not set") - } - - client := openai.NewClient( - option.WithAPIKey(apiKey), - ) - chatCompletion, err := client.Chat.Completions.New(context.TODO(), openai.ChatCompletionNewParams{ - Messages: openai.F([]openai.ChatCompletionMessageParamUnion{ - openai.SystemMessage(fmt.Sprintf(defaultSystemMessage, string(commandType))), - openai.UserMessage(fmt.Sprintf(defaultUserMessage, input, extraContext)), - }), - Model: openai.F(openai.ChatModelGPT4o), - }) - if err != nil { - return ParsedResponse{}, fmt.Errorf("Error from OpenAI API: %v", err) - } - - response := strings.TrimSpace(chatCompletion.Choices[0].Message.Content) - fmt.Printf("AI response: %s\n", response) - parsedResponse, err := parseResponse(response, commandType) - if err != nil { - return ParsedResponse{}, err - } - - return parsedResponse, nil -} - -// parseResponse processes the AI response to extract the command and suggested next steps. -func parseResponse(response string, commandType CommandType) (ParsedResponse, error) { - parsed := ParsedResponse{} - - // Split the response into lines for parsing - lines := strings.Split(response, "\n") - - for _, line := range lines { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "Command:") { - parsed.Command = strings.TrimSpace(strings.TrimPrefix(line, "Command:")) - } else if strings.HasPrefix(line, "Suggested next steps:") { - // Parse the suggestions - suggestions := parseSuggestions(lines) - parsed.SuggestedSteps = suggestions - break - } - } - - // Validate the extracted command - if commandType == KubernetesCommand && !strings.HasPrefix(parsed.Command, "kubectl") { - return ParsedResponse{}, fmt.Errorf("Invalid Kubernetes command: %s", parsed.Command) - } - if commandType == RDBMSQuery { - fmt.Println(parsed.Command) - parsed.Command = cleanSQLQuery(parsed.Command) - if !isValidSQLQuery(parsed.Command) { - return ParsedResponse{}, fmt.Errorf("Invalid SQL query: %s", parsed.Command) - } - } - - return parsed, nil -} - -// parseSuggestions extracts the suggestions from the response. -func parseSuggestions(lines []string) []string { - var suggestions []string - for _, line := range lines { - line = strings.TrimSpace(line) - // Match numbered suggestions (e.g., "1. Describe one of the pods") - if matched, _ := regexp.MatchString(`^\d+\.\s+`, line); matched { - suggestions = append(suggestions, line) - } - } - return suggestions -} - -// cleanSQLQuery processes the SQL query to remove unnecessary formatting and enforce standards. -func cleanSQLQuery(query string) string { - query = strings.TrimPrefix(query, "Query: ") - query = strings.TrimPrefix(query, "```sql") - query = strings.TrimSuffix(query, "```") - query = strings.TrimSpace(query) - return quoteCamelCaseColumns(query) -} - -// isValidSQLQuery checks if the query starts with a valid SQL command. -func isValidSQLQuery(query string) bool { - fmt.Printf("Query: %s\n", query) - validPrefixes := []string{"SELECT", "INSERT", "UPDATE", "DELETE"} - for _, prefix := range validPrefixes { - if strings.HasPrefix(strings.ToUpper(query), prefix) { - return true - } - } - return false -} - -func quoteCamelCaseColumns(query string) string { - // Regular expression to match camel case column names - re := regexp.MustCompile(`\b([a-z]+[A-Z][a-zA-Z0-9]*)\b`) - return re.ReplaceAllStringFunc(query, func(match string) string { - return fmt.Sprintf(`"%s"`, match) - }) -} diff --git a/cmd/connection/kubernetes/create.go b/cmd/connection/kubernetes/create.go index c341e14..9a07c64 100644 --- a/cmd/connection/kubernetes/create.go +++ b/cmd/connection/kubernetes/create.go @@ -27,9 +27,9 @@ func (m *createModel) Init() tea.Cmd { } func (m *createModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg.(type) { + switch msg := msg.(type) { case ui.TransitionToShellMsg: - shellModel := ui.NewShellModel(msg.(ui.TransitionToShellMsg).Connection) + shellModel := ui.NewShellModel(msg.Connection) return shellModel, shellModel.Init() } var cmd tea.Cmd diff --git a/cmd/connection/kubernetes/open.go b/cmd/connection/kubernetes/open.go index 9e673fb..a374686 100644 --- a/cmd/connection/kubernetes/open.go +++ b/cmd/connection/kubernetes/open.go @@ -27,9 +27,9 @@ func (m *openModel) Init() tea.Cmd { } func (m *openModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg.(type) { + switch msg := msg.(type) { case ui.TransitionToShellMsg: - m.current = ui.NewShellModel(msg.(ui.TransitionToShellMsg).Connection) + m.current = ui.NewShellModel(msg.Connection) return m, m.current.Init() } var cmd tea.Cmd diff --git a/make/build.mk b/make/build.mk new file mode 100644 index 0000000..fc605ea --- /dev/null +++ b/make/build.mk @@ -0,0 +1,17 @@ +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) +GOPATH := $(shell go env GOPATH) + +GO111MODULE ?= on +CGO_ENABLED ?= 0 + +.PHONY: build +build: + @echo "Building Prompt-Ops..." + @echo "GOOS: $(GOOS)" + @echo "GOARCH: $(GOARCH)" + @echo "GOPATH: $(GOPATH)" + @echo "GO111MODULE: $(GO111MODULE)" + @echo "CGO_ENABLED: $(CGO_ENABLED)" + @go build -ldflags="-s -w" -o dist/pops-$(GOOS)-$(GOARCH) + @echo "Build complete." \ No newline at end of file diff --git a/make/install.mk b/make/install.mk new file mode 100644 index 0000000..bff7773 --- /dev/null +++ b/make/install.mk @@ -0,0 +1,9 @@ +GOOS ?= $(shell go env GOOS) +GOARCH ?= $(shell go env GOARCH) +GOPATH := $(shell go env GOPATH) + +.PHONY: install +install: build + @echo "Installing Prompt-Ops..." + @cp dist/pops-$(GOOS)-$(GOARCH) /usr/local/bin/pops + @echo "Installation complete." \ No newline at end of file diff --git a/make/lint.mk b/make/lint.mk new file mode 100644 index 0000000..2824a29 --- /dev/null +++ b/make/lint.mk @@ -0,0 +1,13 @@ +GOOS ?= $(shell go env GOOS) + +ifeq ($(GOOS),windows) + GOLANGCI_LINT:=golangci-lint.exe +else + GOLANGCI_LINT:=golangci-lint +endif + +.PHONY: lint +lint: + @echo "Running Go linter..." + @$(GOLANGCI_LINT) run --fix --timeout 5m + @echo "Linting complete." \ No newline at end of file diff --git a/ui/db/create.go b/ui/db/create.go index 5e118a2..aa07307 100644 --- a/ui/db/create.go +++ b/ui/db/create.go @@ -16,7 +16,6 @@ import ( var ( promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")) - outputStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) ) const ( @@ -251,7 +250,7 @@ func (m *createModel) View() string { cursor = "→ " } if i == m.cursor { - s += fmt.Sprintf("%s%s\n", cursor, promptStyle.Copy().Bold(true).Render(dbConn.Subtype)) + s += fmt.Sprintf("%s%s\n", cursor, promptStyle.Bold(true).Render(dbConn.Subtype)) } else { s += fmt.Sprintf("%s%s\n", cursor, promptStyle.Render(dbConn.Subtype)) } diff --git a/ui/kubernetes/create.go b/ui/kubernetes/create.go index 42a5f8d..6881ba9 100644 --- a/ui/kubernetes/create.go +++ b/ui/kubernetes/create.go @@ -17,7 +17,6 @@ import ( // Styles var ( - promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("212")) outputStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("10")) ) diff --git a/ui/kubernetes/open.go b/ui/kubernetes/open.go index 9bcf385..bc424dc 100644 --- a/ui/kubernetes/open.go +++ b/ui/kubernetes/open.go @@ -172,7 +172,7 @@ func (m *openModel) View() string { if m.err != nil { return errorStyle.Render(fmt.Sprintf("❌ Error: %v\n\nPress 'q' or 'esc' to quit.", m.err)) } - return fmt.Sprintf("✅ Connection opened!\n\nPress 'Enter' or 'q'/'esc' to exit.") + return "✅ Connection opened!\n\nPress 'Enter' or 'q'/'esc' to exit." default: return ""