Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ jobs:
- name: Download dependencies
run: go mod download

- name: Install linters
run: make install-lint-tools

- name: Lint
uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6
uses: golangci/golangci-lint-action@0a35821d5c230e903fcfe077583637dea1b27b47 # v9
with:
install-mode: goinstall
version: v1.64.8
install-mode: "none"

test:
name: Test
Expand Down
82 changes: 50 additions & 32 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,59 @@
version: "2"
run:
timeout: 5m
modules-download-mode: readonly

linters-settings:
gci:
sections:
- standard
- default
- prefix(github.com/Use-Tusk/tusk-cli)
gofmt:
simplify: true
goimports:
local-prefixes: github.com/Use-Tusk/tusk-cli
gocritic:
disabled-checks:
- singleCaseSwitch
revive:
rules:
- name: exported
disabled: true

linters:
enable-all: false
disable-all: true
default: none
enable:
- staticcheck
- errcheck
- gosimple
- gocritic
- gosec
- govet
- unused
- ineffassign
- gosec
- gocritic
- misspell
- revive
- staticcheck
- unused
settings:
gocritic:
disabled-checks:
- singleCaseSwitch
staticcheck:
checks:
- all
- -ST1000
- -ST1003
- -ST1005
- -ST1016
- -ST1020
- -ST1021
- -ST1022
revive:
rules:
- name: exported
disabled: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofumpt
- ineffassign
- misspell

issues:
exclude-use-default: false
settings:
gci:
sections:
- standard
- default
- prefix(github.com/Use-Tusk/tusk-cli)
gofmt:
simplify: true
goimports:
local-prefixes:
- github.com/Use-Tusk/tusk-cli
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ GOMOD=$(GOCMD) mod
BINARY_NAME=tusk
BINARY_UNIX=$(BINARY_NAME)_unix

# Tool versions
GOLANGCI_LINT_VERSION=v2.11.4


.PHONY: all build build-ci build-linux test test-ci clean deps install-buf install-lint-tools setup setup-ci run fmt lint help

Expand Down Expand Up @@ -54,7 +57,7 @@ install-buf:
install-lint-tools:
@echo "📦 Installing linting tools..."
GOTOOLCHAIN=local go install mvdan.cc/gofumpt@latest
GOTOOLCHAIN=local go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
GOTOOLCHAIN=local go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
@echo "✅ Linting tools installed"

setup: install-buf deps install-lint-tools
Expand Down
2 changes: 1 addition & 1 deletion cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func getAnthropicAPIConfig() (*APIConfig, error) {

if envKey := os.Getenv("ANTHROPIC_API_KEY"); envKey != "" {
// In non-interactive mode (CI/scripts), default to BYOK to avoid hanging
if !term.IsTerminal(int(os.Stdin.Fd())) {
if !term.IsTerminal(int(os.Stdin.Fd())) { //nolint:gosec // file descriptor fits in int
return &APIConfig{
Mode: agent.APIModeDirect,
APIKey: envKey,
Expand Down
38 changes: 19 additions & 19 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func (a *Agent) trackInterrupted(phaseName string, phasesCompleted int) {
// Run executes the agent with TUI or in headless mode
func (a *Agent) Run(parentCtx context.Context) error {
// Create cancellable context
a.ctx, a.cancel = context.WithCancel(parentCtx)
a.ctx, a.cancel = context.WithCancel(parentCtx) //nolint:gosec // cancel is invoked via defer below
defer a.cancel()

if a.logger != nil {
Expand Down Expand Up @@ -1659,31 +1659,31 @@ func (a *Agent) saveProgress(completedPhases []string, currentPhase string, note
sb.WriteString("## Discovered Information\n\n")
if state != nil && (state.ProjectType != "" || state.PackageManager != "" || state.EntryPoint != "") {
if state.ServiceName != "" {
sb.WriteString(fmt.Sprintf("- **Service Name**: %s\n", state.ServiceName))
fmt.Fprintf(&sb, "- **Service Name**: %s\n", state.ServiceName)
}
if state.ProjectType != "" {
sb.WriteString(fmt.Sprintf("- **Project Type**: %s\n", state.ProjectType))
fmt.Fprintf(&sb, "- **Project Type**: %s\n", state.ProjectType)
}
if state.PackageManager != "" {
sb.WriteString(fmt.Sprintf("- **Package Manager**: %s\n", state.PackageManager))
fmt.Fprintf(&sb, "- **Package Manager**: %s\n", state.PackageManager)
}
if state.ModuleSystem != "" {
sb.WriteString(fmt.Sprintf("- **Module System**: %s\n", state.ModuleSystem))
fmt.Fprintf(&sb, "- **Module System**: %s\n", state.ModuleSystem)
}
if state.EntryPoint != "" {
sb.WriteString(fmt.Sprintf("- **Entry Point**: %s\n", state.EntryPoint))
fmt.Fprintf(&sb, "- **Entry Point**: %s\n", state.EntryPoint)
}
if state.StartCommand != "" {
sb.WriteString(fmt.Sprintf("- **Start Command**: `%s`\n", state.StartCommand))
fmt.Fprintf(&sb, "- **Start Command**: `%s`\n", state.StartCommand)
}
if state.Port != "" {
sb.WriteString(fmt.Sprintf("- **Port**: %s\n", state.Port))
fmt.Fprintf(&sb, "- **Port**: %s\n", state.Port)
}
if state.HealthEndpoint != "" {
sb.WriteString(fmt.Sprintf("- **Health Endpoint**: %s\n", state.HealthEndpoint))
fmt.Fprintf(&sb, "- **Health Endpoint**: %s\n", state.HealthEndpoint)
}
if state.DockerType != "" && state.DockerType != "none" {
sb.WriteString(fmt.Sprintf("- **Docker**: %s\n", state.DockerType))
fmt.Fprintf(&sb, "- **Docker**: %s\n", state.DockerType)
}
sb.WriteString("\n")
} else {
Expand All @@ -1695,7 +1695,7 @@ func (a *Agent) saveProgress(completedPhases []string, currentPhase string, note
sb.WriteString("The following packages are used but not instrumented by the SDK.\n")
sb.WriteString("Recording/replay may not capture these calls:\n\n")
for _, warning := range state.CompatibilityWarnings {
sb.WriteString(fmt.Sprintf("- ⚠️ %s\n", warning))
fmt.Fprintf(&sb, "- ⚠️ %s\n", warning)
}
sb.WriteString("\n")
}
Expand Down Expand Up @@ -1725,10 +1725,10 @@ func (a *Agent) saveProgress(completedPhases []string, currentPhase string, note
sb.WriteString("- ✓ Authenticated with Tusk Cloud\n")
}
if state.GitRepoOwner != "" && state.GitRepoName != "" {
sb.WriteString(fmt.Sprintf("- ✓ Repository detected: %s/%s\n", state.GitRepoOwner, state.GitRepoName))
fmt.Fprintf(&sb, "- ✓ Repository detected: %s/%s\n", state.GitRepoOwner, state.GitRepoName)
}
if state.CloudServiceID != "" {
sb.WriteString(fmt.Sprintf("- ✓ Cloud service created (ID: %s)\n", state.CloudServiceID))
fmt.Fprintf(&sb, "- ✓ Cloud service created (ID: %s)\n", state.CloudServiceID)
}
if state.ApiKeyCreated {
sb.WriteString("- ✓ API key created\n")
Expand All @@ -1741,31 +1741,31 @@ func (a *Agent) saveProgress(completedPhases []string, currentPhase string, note
sb.WriteString("None yet.\n\n")
} else {
for _, phase := range completedPhases {
sb.WriteString(fmt.Sprintf("- ✓ %s\n", phase))
fmt.Fprintf(&sb, "- ✓ %s\n", phase)
}
sb.WriteString("\n")
}

if currentPhase != "" {
sb.WriteString(fmt.Sprintf("## Current Phase\n\n%s (in progress)\n\n", currentPhase))
fmt.Fprintf(&sb, "## Current Phase\n\n%s (in progress)\n\n", currentPhase)
}

if state != nil && (len(state.Errors) > 0 || len(state.Warnings) > 0) {
if len(state.Errors) > 0 {
sb.WriteString("## Errors Encountered\n\n")
for _, err := range state.Errors {
if err.Fatal {
sb.WriteString(fmt.Sprintf("- ❌ [%s] %s (fatal)\n", err.Phase, err.Message))
fmt.Fprintf(&sb, "- ❌ [%s] %s (fatal)\n", err.Phase, err.Message)
} else {
sb.WriteString(fmt.Sprintf("- ⚠️ [%s] %s\n", err.Phase, err.Message))
fmt.Fprintf(&sb, "- ⚠️ [%s] %s\n", err.Phase, err.Message)
}
}
sb.WriteString("\n")
}
if len(state.Warnings) > 0 {
sb.WriteString("## Warnings\n\n")
for _, w := range state.Warnings {
sb.WriteString(fmt.Sprintf("- %s\n", w))
fmt.Fprintf(&sb, "- %s\n", w)
}
sb.WriteString("\n")
}
Expand All @@ -1777,7 +1777,7 @@ func (a *Agent) saveProgress(completedPhases []string, currentPhase string, note
sb.WriteString("\n\n")
}

sb.WriteString(fmt.Sprintf("---\nLast updated: %s\n", time.Now().Format(time.RFC3339)))
fmt.Fprintf(&sb, "---\nLast updated: %s\n", time.Now().Format(time.RFC3339))

return os.WriteFile(a.progressFilePath(), []byte(sb.String()), 0o600)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/agent/phases.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func (pm *PhaseManager) UpdateState(results map[string]interface{}) {

// StateAsContext returns the current state as a string for the prompt
func (pm *PhaseManager) StateAsContext() string {
data, _ := json.MarshalIndent(pm.state, "", " ")
data, _ := json.MarshalIndent(pm.state, "", " ") //nolint:gosec // intentional serialization of agent state
result := string(data)

// Include previous progress if available
Expand Down Expand Up @@ -1027,10 +1027,10 @@ func eligibilityCheckPhase() *Phase {
}
manifest, err := tools.FetchManifestFromURL(url)
if err != nil {
extra.WriteString(fmt.Sprintf("**%s**: Failed to fetch manifest - %s\n\n", lang, err))
fmt.Fprintf(&extra, "**%s**: Failed to fetch manifest - %s\n\n", lang, err)
continue
}
extra.WriteString(fmt.Sprintf("**%s Manifest**:\n```json\n%s\n```\n\n", lang, manifest))
fmt.Fprintf(&extra, "**%s Manifest**:\n```json\n%s\n```\n\n", lang, manifest)
}

// Add user guidance if provided
Expand Down
2 changes: 1 addition & 1 deletion internal/agent/tools/abort.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func ResetPhaseProgress(workDir string) func(json.RawMessage) (string, error) {
}

newContent := strings.Join(newLines, "\n")
if err := os.WriteFile(progressPath, []byte(newContent), 0o600); err != nil {
if err := os.WriteFile(progressPath, []byte(newContent), 0o600); err != nil { //nolint:gosec // path is constructed internally for agent progress tracking
return "", err
}

Expand Down
2 changes: 1 addition & 1 deletion internal/agent/tools/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func (ft *FilesystemTools) PatchFile(input json.RawMessage) (string, error) {
modified += "\n"
}

if err := os.WriteFile(fullPath, []byte(modified), 0o600); err != nil {
if err := os.WriteFile(fullPath, []byte(modified), 0o600); err != nil { //nolint:gosec // agent tool deliberately writes to user-specified paths
return "", fmt.Errorf("failed to write file: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion internal/agent/ui_headless.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func (u *HeadlessUI) AgentText(text string, streaming bool) {
if strings.TrimSpace(text) != "" {
width := 90
if utils.IsTerminal() {
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 {
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 { //nolint:gosec // file descriptor fits in int
width = max(w-4, 40)
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/analytics/notice.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func ShowFirstRunNotice(cmd *cobra.Command) bool {
}

// Skip if not a TTY (piped output)
if !term.IsTerminal(int(os.Stdout.Fd())) {
if !term.IsTerminal(int(os.Stdout.Fd())) { //nolint:gosec // file descriptor fits in int
return false
}

Expand Down
2 changes: 1 addition & 1 deletion internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func buildAuthenticatedRequest(
}

func (c *TuskClient) executeRequest(httpReq *http.Request) ([]byte, *http.Response, error) {
httpResp, err := c.httpClient.Do(httpReq)
httpResp, err := c.httpClient.Do(httpReq) //nolint:gosec // request URL is configured by the CLI, not user-controlled input
if err != nil {
return nil, nil, fmt.Errorf("http error: %w", err)
}
Expand Down
8 changes: 4 additions & 4 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func (a *Authenticator) SaveTokenFile() error {
if err := os.MkdirAll(dir, 0o700); err != nil {
return fmt.Errorf("cannot create config dir %q: %w", dir, err)
}
b, err := json.MarshalIndent(a.Token, "", " ")
b, err := json.MarshalIndent(a.Token, "", " ") //nolint:gosec // intentional persistence of auth token to user config dir
if err != nil {
return err
}
Expand All @@ -113,11 +113,11 @@ func (a *Authenticator) SaveTokenFile() error {
func openBrowser(link string) error {
switch runtime.GOOS {
case "darwin":
return exec.Command("open", link).Start()
return exec.Command("open", link).Start() //nolint:gosec // opening URL in browser is intentional
case "windows":
return exec.Command("rundll32", "url.dll,FileProtocolHandler", link).Start()
return exec.Command("rundll32", "url.dll,FileProtocolHandler", link).Start() //nolint:gosec // opening URL in browser is intentional
default:
return exec.Command("xdg-open", link).Start()
return exec.Command("xdg-open", link).Start() //nolint:gosec // opening URL in browser is intentional
}
}

Expand Down
Loading
Loading