diff --git a/.gitignore b/.gitignore index b94e0a2..e99e43d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,4 @@ go.work.sum # .vscode/ brainstorm .idea -lazyssh \ No newline at end of file +bin \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..6b0f0f6 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,105 @@ +run: + timeout: 5m + allow-parallel-runners: true + +issues: + # disable the default set of exclude rules + exclude-use-default: false + + exclude-rules: + # disable some linters for api packages + - path: "api/*" + linters: + - lll + + # disable some linters for internal packages + - path: "internal/*" + linters: + - dupl + - lll + + # exclude some linters for the test directory and test files + - path: test/.*|.*_test\.go + linters: + - dupl + - errcheck + - goconst + - gocyclo + - gosec + +linters: + disable-all: true + enable: + - asciicheck + - bodyclose + - copyloopvar + - dogsled + - dupl + - errcheck + - errorlint + - exhaustive + - ginkgolinter + - goconst + - gocritic + - gocyclo + - gofmt + - goheader + - goimports + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - nolintlint + - prealloc + - revive + - staticcheck + - typecheck + - unconvert + - unparam + - unused + +linters-settings: + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - assignOp + - filepathJoin + - paramTypeCombine + - rangeValCopy + - unnamedResult + - whyNoLint + + goimports: + local-prefixes: github.com/kubeflow/notebooks/workspaces/backend + + goheader: + template: |- + Copyright 2024. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + misspell: + locale: US + + revive: + rules: + - name: comment-spacings diff --git a/cmd/main.go b/cmd/main.go index 616f392..f0c3f62 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,18 +2,25 @@ package main import ( "fmt" + "os" + "time" + "github.com/Adembc/lazyssh/internal/adapters/data/memory" "github.com/Adembc/lazyssh/internal/adapters/ui" "github.com/Adembc/lazyssh/internal/core/services" "github.com/spf13/cobra" - "os" +) + +var ( + version = "develop" + gitCommit = "unknown" + buildTime = time.Now().Format("2006-01-02 15:04:05") ) func main() { - serverInMemoryRepo := memory.NewServerRepository() serverService := services.NewServerService(serverInMemoryRepo) - tui := ui.NewTUI(serverService) + tui := ui.NewTUI(serverService, version, gitCommit, buildTime) rootCmd := &cobra.Command{ Use: ui.AppName, @@ -25,7 +32,7 @@ func main() { rootCmd.SilenceUsage = true if err := rootCmd.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) + _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) } } diff --git a/go.mod b/go.mod index 10355b6..6aef415 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.4 require ( github.com/gdamore/tcell/v2 v2.8.1 github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb + github.com/spf13/cobra v1.9.1 ) require ( @@ -13,9 +14,8 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.7 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/term v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect ) diff --git a/go.sum b/go.sum index 78e12fe..411851a 100644 --- a/go.sum +++ b/go.sum @@ -58,8 +58,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -68,8 +69,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -78,8 +80,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/adapters/data/memory/server_repo.go b/internal/adapters/data/memory/server_repo.go index e5f24e5..2feacf5 100644 --- a/internal/adapters/data/memory/server_repo.go +++ b/internal/adapters/data/memory/server_repo.go @@ -1,14 +1,14 @@ package memory import ( - "github.com/Adembc/lazyssh/internal/core/domain" "strconv" "strings" "time" + + "github.com/Adembc/lazyssh/internal/core/domain" ) -type serverRepository struct { -} +type serverRepository struct{} var servers = []domain.Server{ {Alias: "web-01", Host: "192.168.1.10", User: "root", Port: 22, Key: "~/.ssh/id_rsa", Tags: []string{"prod", "web"}, Status: "online", LastSeen: time.Now().Add(-2 * time.Hour)}, @@ -58,7 +58,6 @@ func (r *serverRepository) ListServers(query string) ([]domain.Server, error) { } } return filteredServers, nil - return servers, nil } // UpdateServer updates an existing server with new details. diff --git a/internal/adapters/ui/const.go b/internal/adapters/ui/const.go index 8fb4af7..7f87611 100644 --- a/internal/adapters/ui/const.go +++ b/internal/adapters/ui/const.go @@ -4,7 +4,6 @@ import "time" const ( AppName = "lazyssh" - AppVersion = "v0.0.1" RepoURL = "github.com/adembc/lazyssh" SplashScreenDuration = 1 * time.Second Banner = ` diff --git a/internal/adapters/ui/handlers.go b/internal/adapters/ui/handlers.go index 4f981e8..aa7a087 100644 --- a/internal/adapters/ui/handlers.go +++ b/internal/adapters/ui/handlers.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "github.com/Adembc/lazyssh/internal/core/domain" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" @@ -87,10 +88,10 @@ func (t *tui) handleServerEdit() { func (t *tui) handleServerSave(server domain.Server, original *domain.Server) { if original != nil { // Edit mode - t.serverService.UpdateServer(*original, server) + _ = t.serverService.UpdateServer(*original, server) } else { // Add mode - t.serverService.AddServer(server) + _ = t.serverService.AddServer(server) } t.refreshServerList() diff --git a/internal/adapters/ui/header.go b/internal/adapters/ui/header.go index 6d45c22..3d78e5f 100644 --- a/internal/adapters/ui/header.go +++ b/internal/adapters/ui/header.go @@ -1,23 +1,28 @@ package ui import ( - "github.com/gdamore/tcell/v2" - "github.com/rivo/tview" "strings" "time" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" ) type AppHeader struct { *tview.Flex - version string - repoURL string + version string + buildTime string + gitCommit string + repoURL string } -func NewAppHeader(version, repoURL string) *AppHeader { +func NewAppHeader(version, gitCommit, buildTime, repoURL string) *AppHeader { header := &AppHeader{ - Flex: tview.NewFlex(), - version: version, - repoURL: repoURL, + Flex: tview.NewFlex(), + version: version, + repoURL: repoURL, + buildTime: buildTime, + gitCommit: gitCommit, } header.build() return header @@ -57,7 +62,24 @@ func (h *AppHeader) buildCenterSection(bg tcell.Color) *tview.TextView { SetDynamicColors(true). SetTextAlign(tview.AlignCenter) center.SetBackgroundColor(bg) - center.SetText("[black:#2ECC71::b] " + h.version + " [-] [black:#3B82F6::b] TUI [-]") + + commit := shortCommit(h.gitCommit) + + // Build tag-like chips for version, commit, and build time + versionTag := makeTag(h.version, "#22C55E") // green + commitTag := "" + if commit != "" { + commitTag = makeTag(commit, "#A78BFA") // violet + } + timeTag := makeTag(formatBuildTime(h.buildTime), "#3B82F6") // blue + + text := versionTag + if commitTag != "" { + text += " " + commitTag + } + text += " " + timeTag + + center.SetText(text) return center } @@ -77,3 +99,48 @@ func (h *AppHeader) createSeparator() *tview.TextView { separator.SetText("[#444444]" + strings.Repeat("─", 200) + "[-]") return separator } + +// shortCommit returns first 7 chars of commit if it looks valid; otherwise empty string. +func shortCommit(c string) string { + c = strings.TrimSpace(c) + if c == "" || c == "unknown" || c == "(devel)" { + return "" + } + if len(c) > 7 { + return c[:7] + } + return c +} + +// formatBuildTime tries to parse common time formats and returns a concise human-readable string. +func formatBuildTime(s string) string { + s = strings.TrimSpace(s) + if s == "" { + return "unknown" + } + layouts := []string{ + "2006-01-02 15:04:05", + time.RFC3339, + time.RFC1123, + time.RFC1123Z, + } + var t time.Time + var err error + for _, l := range layouts { + t, err = time.Parse(l, s) + if err == nil { + return t.Format("Mon, 02 Jan 2006 15:04") + } + } + + return s +} + +// makeTag returns a rectangular-looking colored chip for the given text. +func makeTag(text, bg string) string { + text = strings.TrimSpace(text) + if text == "" { + return "" + } + return "[black:" + bg + "::b] " + text + " [-]" +} diff --git a/internal/adapters/ui/server_details.go b/internal/adapters/ui/server_details.go index 1761273..6b17029 100644 --- a/internal/adapters/ui/server_details.go +++ b/internal/adapters/ui/server_details.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "github.com/Adembc/lazyssh/internal/core/domain" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" diff --git a/internal/adapters/ui/server_form.go b/internal/adapters/ui/server_form.go index b6e5f93..db9218b 100644 --- a/internal/adapters/ui/server_form.go +++ b/internal/adapters/ui/server_form.go @@ -2,12 +2,13 @@ package ui import ( "fmt" - "github.com/Adembc/lazyssh/internal/core/domain" - "github.com/gdamore/tcell/v2" - "github.com/rivo/tview" "strconv" "strings" "time" + + "github.com/Adembc/lazyssh/internal/core/domain" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" ) type ServerFormMode int diff --git a/internal/adapters/ui/splash.go b/internal/adapters/ui/splash.go index 8f79b41..8193d2f 100644 --- a/internal/adapters/ui/splash.go +++ b/internal/adapters/ui/splash.go @@ -1,10 +1,11 @@ package ui import ( - "github.com/gdamore/tcell/v2" - "github.com/rivo/tview" "strings" "time" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" ) // buildSplash creates a splash screen with a spinner; returns primitive and a stop function diff --git a/internal/adapters/ui/tui.go b/internal/adapters/ui/tui.go index 70a9fdf..35a4a0d 100644 --- a/internal/adapters/ui/tui.go +++ b/internal/adapters/ui/tui.go @@ -1,13 +1,18 @@ package ui import ( + "time" + "github.com/Adembc/lazyssh/internal/core/ports" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" - "time" ) type tui struct { + version string + commit string + buildDate string + app *tview.Application serverService ports.ServerService @@ -25,21 +30,23 @@ type tui struct { searchVisible bool } -func NewTUI(ss ports.ServerService) *tui { +func NewTUI(ss ports.ServerService, version, commit, buildDate string) *tui { return &tui{ app: tview.NewApplication(), serverService: ss, + version: version, + commit: commit, + buildDate: buildDate, } - } func (t *tui) Run() error { - t.app.EnableMouse(true) t.initializeTheme().buildComponents().buildLayout().bindEvents().loadInitialData().loadSplashScreen() return t.app.Run() } + func (t *tui) initializeTheme() *tui { tview.Styles.PrimitiveBackgroundColor = tcell.Color232 tview.Styles.ContrastBackgroundColor = tcell.Color235 @@ -51,8 +58,9 @@ func (t *tui) initializeTheme() *tui { tview.Styles.GraphicsColor = tcell.Color238 return t } + func (t *tui) buildComponents() *tui { - t.header = NewAppHeader(AppVersion, RepoURL) + t.header = NewAppHeader(t.version, t.commit, t.buildDate, RepoURL) t.searchBar = NewSearchBar(). OnSearch(t.handleSearchInput). OnEscape(t.hideSearchBar) @@ -63,8 +71,8 @@ func (t *tui) buildComponents() *tui { t.statusBar = NewStatusBar() return t } -func (t *tui) buildLayout() *tui { +func (t *tui) buildLayout() *tui { t.left = tview.NewFlex().SetDirection(tview.FlexRow). AddItem(t.hintBar, 1, 0, false). AddItem(t.serverList, 0, 1, true) @@ -82,10 +90,12 @@ func (t *tui) buildLayout() *tui { AddItem(t.statusBar, 1, 0, false) return t } + func (t *tui) bindEvents() *tui { t.root.SetInputCapture(t.handleGlobalKeys) return t } + func (t *tui) loadInitialData() *tui { servers, _ := t.serverService.ListServers("") t.serverList.UpdateServers(servers) @@ -103,5 +113,4 @@ func (t *tui) loadSplashScreen() *tui { }) }) return t - } diff --git a/internal/adapters/ui/utils.go b/internal/adapters/ui/utils.go index d4f0f9d..9b1f129 100644 --- a/internal/adapters/ui/utils.go +++ b/internal/adapters/ui/utils.go @@ -2,9 +2,10 @@ package ui import ( "fmt" - "github.com/Adembc/lazyssh/internal/core/domain" "strings" "time" + + "github.com/Adembc/lazyssh/internal/core/domain" ) func statusIcon(s string) string { diff --git a/internal/core/services/server_service.go b/internal/core/services/server_service.go index c9feb1a..7e6db5c 100644 --- a/internal/core/services/server_service.go +++ b/internal/core/services/server_service.go @@ -18,7 +18,6 @@ func NewServerService(sr ports.ServerRepository) *serverService { // ListServers returns a list of servers. func (s *serverService) ListServers(query string) ([]domain.Server, error) { - // do any relevant business logic here if needed servers, err := s.serverRepository.ListServers(query) if err != nil { diff --git a/makefile b/makefile index 4f898e6..a7b0c87 100644 --- a/makefile +++ b/makefile @@ -1,4 +1,210 @@ -build: - go build -o lazyssh ./cmd -run: - go run ./cmd +# Makefile for LazySSH project + +##@ General + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +# Default target +.DEFAULT_GOAL := help + +# Project variables +PROJECT_NAME ?= $(shell basename $(CURDIR)) +VERSION ?= v0.0.1 +BUILD_TIME ?= $(shell date -u '+%Y-%m-%d_%H:%M:%S') +GIT_COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") + +# Build variables +BINARY_NAME ?= lazyssh +OUTPUT_DIR ?= ./bin +CMD_DIR ?= ./cmd +PKG_LIST := $(shell go list ./...) + +# LDFLAGS for version information +LDFLAGS = -ldflags "-X main.version=$(VERSION) -X main.buildTime=$(BUILD_TIME) -X main.gitCommit=$(GIT_COMMIT)" + +##@ Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(shell pwd)/bin +$(LOCALBIN): + mkdir -p $(LOCALBIN) + +# Tool versions +GOLANGCI_LINT_VERSION ?= v1.61.0 +GOFUMPT_VERSION ?= v0.7.0 +STATICCHECK_VERSION ?= 2024.1.1 + +# Tool binaries +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint +GOFUMPT = $(LOCALBIN)/gofumpt +STATICCHECK = $(LOCALBIN)/staticcheck + +# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# $1 - target path with name of binary +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f "$(1)-$(3)" ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +rm -f $(1) || true ;\ +GOBIN=$(LOCALBIN) go install $${package} ;\ +mv $(1) $(1)-$(3) ;\ +} ;\ +ln -sf $(1)-$(3) $(1) +endef + +.PHONY: tools +tools: golangci-lint gofumpt staticcheck ## Install all development tools + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary +$(GOLANGCI_LINT): $(LOCALBIN) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + +.PHONY: gofumpt +gofumpt: $(GOFUMPT) ## Download gofumpt locally if necessary +$(GOFUMPT): $(LOCALBIN) + $(call go-install-tool,$(GOFUMPT),mvdan.cc/gofumpt,$(GOFUMPT_VERSION)) + +.PHONY: staticcheck +staticcheck: $(STATICCHECK) ## Download staticcheck locally if necessary +$(STATICCHECK): $(LOCALBIN) + $(call go-install-tool,$(STATICCHECK),honnef.co/go/tools/cmd/staticcheck,$(STATICCHECK_VERSION)) + +##@ Development + +.PHONY: fmt +fmt: gofumpt ## Format Go code + $(GOFUMPT) -l -w . + go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code + go vet ./... + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter + $(GOLANGCI_LINT) run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + $(GOLANGCI_LINT) run --fix + +.PHONY: check +check: staticcheck ## Run staticcheck analyzer + $(STATICCHECK) ./... + +.PHONY: quality +quality: fmt vet ## Run all code quality checks (i will add lint and check) + +##@ Testing + +.PHONY: test +test: ## Run unit tests + go test -race -coverprofile=coverage.out ./... + +.PHONY: test-verbose +test-verbose: ## Run unit tests with verbose output + go test -race -v -coverprofile=coverage.out ./... + +.PHONY: test-short +test-short: ## Run unit tests (short mode) + go test -race -short ./... + +.PHONY: coverage +coverage: test ## Run tests and show coverage + go tool cover -html=coverage.out -o coverage.html + @echo "Coverage report generated: coverage.html" + +.PHONY: benchmark +benchmark: ## Run benchmarks + go test -bench=. -benchmem ./... + +##@ Building + +.PHONY: deps +deps: ## Download dependencies + go mod download + go mod verify + +.PHONY: tidy +tidy: ## Tidy up dependencies + go mod tidy + +.PHONY: build +build: quality $(OUTPUT_DIR) ## Build binary + go build $(LDFLAGS) -o $(OUTPUT_DIR)/$(BINARY_NAME) $(CMD_DIR) + +.PHONY: build-all +build-all: quality $(OUTPUT_DIR) ## Build binaries for all platforms + GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(OUTPUT_DIR)/$(BINARY_NAME)-linux-amd64 $(CMD_DIR) + GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o $(OUTPUT_DIR)/$(BINARY_NAME)-linux-arm64 $(CMD_DIR) + GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(OUTPUT_DIR)/$(BINARY_NAME)-darwin-amd64 $(CMD_DIR) + GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(OUTPUT_DIR)/$(BINARY_NAME)-darwin-arm64 $(CMD_DIR) + GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(OUTPUT_DIR)/$(BINARY_NAME)-windows-amd64.exe $(CMD_DIR) + +.PHONY: install +install: build ## Install binary to GOBIN + cp $(OUTPUT_DIR)/$(BINARY_NAME) $(GOBIN)/ + +$(OUTPUT_DIR): + mkdir -p $(OUTPUT_DIR) + +##@ Running + +.PHONY: run +run: ## Run application from source + go run $(CMD_DIR)/main.go + +.PHONY: run-race +run-race: ## Run application from source with race detector + go run -race $(CMD_DIR)/main.go + +##@ Maintenance + +.PHONY: clean +clean: ## Clean build artifacts and caches + go clean -cache -testcache -modcache + rm -rf $(OUTPUT_DIR) + rm -rf $(LOCALBIN) + rm -f coverage.out coverage.html + +.PHONY: clean-build +clean-build: ## Clean only build artifacts + rm -rf $(OUTPUT_DIR) + rm -f coverage.out coverage.html + +.PHONY: update-deps +update-deps: ## Update all dependencies + go get -u ./... + go mod tidy + +.PHONY: security +security: ## Run security checks + go list -json -deps ./... | grep -v "$$GOROOT" | jq -r '.Module | select(.Path != null) | .Path' | sort -u | xargs go list -json -m | jq -r 'select(.Replace == null) | "\(.Path)@\(.Version)"' | xargs -I {} sh -c 'echo "Checking {}" && go list -json -m {} | jq -r .Dir' >/dev/null + +.PHONY: version +version: ## Display version information + @echo "Project: $(PROJECT_NAME)" + @echo "Version: $(VERSION)" + @echo "Build Time: $(BUILD_TIME)" + @echo "Git Commit: $(GIT_COMMIT)" + @echo "Go Version: $(shell go version)" + +##@ Help + +.PHONY: help +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) \ No newline at end of file