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
86 changes: 86 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
#
# Pre-commit hook for Seedfast CLI
#
# Enable by running: git config core.hooksPath .githooks
#

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color

failed=0

echo "Running pre-commit checks..."

# 1. Check formatting on staged .go files only
staged_go_files=$(git diff --cached --name-only --diff-filter=ACM -- '*.go' || true)
if [ -n "$staged_go_files" ]; then
unformatted=""
for file in $staged_go_files; do
if [ -f "$file" ]; then
output=$(gofmt -l "$file" 2>/dev/null || true)
if [ -n "$output" ]; then
unformatted="$unformatted\n $file"
fi
fi
done
if [ -n "$unformatted" ]; then
echo -e "${RED}FAIL${NC} go fmt: unformatted files detected:${unformatted}"
echo " Run: gofmt -w . (or go fmt ./...)"
failed=1
else
echo -e "${GREEN}PASS${NC} go fmt"
fi
else
echo -e "${GREEN}SKIP${NC} go fmt (no staged .go files)"
fi

# 2. go vet
echo -n "Running go vet... "
if ! go vet ./... 2>&1; then
echo -e "${RED}FAIL${NC} go vet"
failed=1
else
echo -e "${GREEN}PASS${NC} go vet"
fi

# 3. Production build
echo -n "Running go build... "
if ! go build ./... 2>&1; then
echo -e "${RED}FAIL${NC} go build (production)"
failed=1
else
echo -e "${GREEN}PASS${NC} go build (production)"
fi

# 4. Dev build
echo -n "Running go build -tags dev... "
if ! go build -tags dev ./... 2>&1; then
echo -e "${RED}FAIL${NC} go build -tags dev"
failed=1
else
echo -e "${GREEN}PASS${NC} go build -tags dev"
fi

# 5. Check for staged docs/ files
staged_docs=$(git diff --cached --name-only -- 'docs/' || true)
if [ -n "$staged_docs" ]; then
echo -e "${RED}FAIL${NC} docs/ files staged for commit (internal docs, not for public repo):"
echo "$staged_docs" | sed 's/^/ /'
echo " Unstage with: git reset HEAD docs/"
failed=1
else
echo -e "${GREEN}PASS${NC} no docs/ files staged"
fi

if [ "$failed" -ne 0 ]; then
echo ""
echo -e "${RED}Pre-commit checks failed.${NC} Fix the issues above and try again."
exit 1
fi

echo ""
echo -e "${GREEN}All pre-commit checks passed.${NC}"
38 changes: 36 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ permissions:
contents: write

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25.1'
cache: true

- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest

ci:
name: CI
runs-on: ubuntu-latest
Expand Down Expand Up @@ -59,5 +77,21 @@ jobs:
- name: Vet
run: go vet ./...

- name: Test
run: go test ./... -timeout 300s
- name: Test with coverage
run: go test -coverprofile=coverage.out -timeout 300s ./...

- name: Coverage summary
if: always()
run: |
if [ -f coverage.out ]; then
echo "### Coverage Summary"
go tool cover -func=coverage.out | tail -1
fi

- name: Upload coverage profile
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-profile
path: coverage.out
retention-days: 30
20 changes: 20 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
run:
timeout: 3m
go: "1.24"

linters:
enable:
- errcheck
- ineffassign
- staticcheck
- unused
- govet
- gofmt

issues:
exclude-files:
- ".*\\.pb\\.go$"
exclude-rules:
- path: _test\.go
linters:
- errcheck
18 changes: 8 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,14 @@ The main Claude Code session acts as the team lead. It coordinates all work by s

Sequential issue pipeline:

1. **Coder** implements fix -> proactively notifies tester
2. **Tester** designs test plan:
- Define precise expected behavior for each scenario (not "one of these outcomes is fine")
- Proactively consult KB manager for existing knowledge about how the feature should work
- Cover edge cases aggressively -- that's the whole point of a test plan
- Agree expected behavior with team lead before executing
- Never test log levels or implementation details -- only observable behavior
- Submit plan to team lead for approval -> execute after approval
3. **KB manager** proactively updates knowledge base once testing passes
4. Team proceeds to next issue
1. **Coder** implements fix → proactively notifies tester
2. **Tester** designs test plan → submits to team lead for approval → executes after approval
3. **Test results:**
- **PASS** → KB manager updates docs → proceed to next issue
- **FAIL** → tester sends structured error report (see go-tester.md template) → coder
4. **Coder** fixes based on structured report → notifies tester (retry from step 3)
5. **Max 3 iterations** — if tests still fail after 3 coder→tester cycles, escalate to team lead
6. **Team lead** decides: alternative approach, accept with known issues, or architectural change

### Key Team Rules

Expand Down
6 changes: 1 addition & 5 deletions cmd/grpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,8 @@ var grpcTestCmd = &cobra.Command{
fmt.Println("\nStep 5: Testing gRPC connection...")
tlsCreds := credentials.NewTLS(tlsCfg)

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

grpcConn, err := grpc.DialContext(ctx, grpcAddr,
grpcConn, err := grpc.NewClient(grpcAddr,
grpc.WithTransportCredentials(tlsCreds),
grpc.WithBlock(),
)
if err != nil {
fmt.Printf("❌ gRPC dial failed: %v\n", err)
Expand Down
29 changes: 0 additions & 29 deletions cmd/seed_dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,3 @@
//go:build dev

package cmd

import (
"seedfast/cli/internal/manifest"

"github.com/pterm/pterm"
)

// showEndpointIndicators displays visual indicators for endpoint sources in dev builds.
// Shows [PROD] if using production endpoints, or [CUSTOM] if using custom build-time endpoints.
func showEndpointIndicators() {
if manifest.CustomBackendURL != "" || manifest.CustomGrpcURL != "" {
// Custom endpoints - at least one build-time variable is set
pterm.Println(pterm.NewStyle(pterm.FgYellow, pterm.Bold).Sprint("[CUSTOM]") +
pterm.NewStyle(pterm.FgYellow).Sprint(" Using custom endpoints:"))

if manifest.CustomBackendURL != "" {
pterm.Printf(" → Backend: %s\n", manifest.CustomBackendURL)
}
if manifest.CustomGrpcURL != "" {
pterm.Printf(" → gRPC: %s\n", manifest.CustomGrpcURL)
}
pterm.Println()
} else {
// Production endpoints - both build-time variables are empty
pterm.Println(pterm.NewStyle(pterm.FgGreen, pterm.Bold).Sprint("[PROD]") +
pterm.NewStyle(pterm.FgGreen).Sprint(" Using production endpoints"))
pterm.Println()
}
}
6 changes: 0 additions & 6 deletions cmd/seed_prod.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,3 @@
//go:build !dev

package cmd

// showEndpointIndicators is a no-op in production builds.
// No visual indicators are shown for endpoint sources.
func showEndpointIndicators() {
// No-op in production builds
}
6 changes: 0 additions & 6 deletions cmd/seed_tui_prod.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,3 @@
//go:build !dev

package cmd

// isTUIOutputMode always returns false in production builds.
// The TUI renderer is only available in dev builds.
func isTUIOutputMode() bool {
return false
}
4 changes: 2 additions & 2 deletions cmd/tuidemo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func runInteractive(p *tea.Program) {
for _, ts := range tables {
p.Send(tui.TableStartedMsg{Name: ts.Name, Remaining: len(tables)})
sleep(randMs(800, 1500))
p.Send(tui.TableDoneMsg{Name: ts.Name, RecordsCount: ts.RecordsCount})
p.Send(tui.TableDoneMsg(ts))
}

sleep(300)
Expand Down Expand Up @@ -303,7 +303,7 @@ func sendSeedingFlow(p *tea.Program, tables []tui.TableSpec, failures map[string
if reason, failed := failures[ts.Name]; failed {
p.Send(tui.TableFailedMsg{Name: ts.Name, Reason: reason})
} else {
p.Send(tui.TableDoneMsg{Name: ts.Name, RecordsCount: ts.RecordsCount})
p.Send(tui.TableDoneMsg(ts))
}
}

Expand Down
56 changes: 0 additions & 56 deletions cmd/ui_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,3 @@
// Licensed under the MIT License. See LICENSE file in the project root for details.

package cmd

import (
"fmt"
"io"
"sync"
"time"
)

// startInlineSpinner starts a simple inline spinner animation on a single line.
// It displays rotating animation frames followed by the provided text, updating
// the same line in the terminal. The spinner runs in a separate goroutine and
// can be stopped by calling the returned function.
//
// The spinner automatically clears the line when stopped and handles text length
// limits to prevent display issues. It uses the provided frames array for animation
// and updates at the specified interval.
//
// Parameters:
// - w: The io.Writer to write the spinner to (typically os.Stdout or os.Stderr)
// - text: The text to display after the spinner animation
// - frames: Array of strings representing animation frames (e.g., ["|", "/", "-", "\\"])
// - interval: Time duration between frame updates
//
// Returns a function that stops the spinner and cleans up when called.
func startInlineSpinner(w io.Writer, text string, frames []string, interval time.Duration) func() {
stop := make(chan struct{})
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
i := 0
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-stop:
line := fmt.Sprintf("%s %s", frames[i%len(frames)], text)
// Clear the spinner line completely, then return
fmt.Fprintf(w, "\r%*s\r", len(line), "")
return
case <-ticker.C:
line := fmt.Sprintf("%s %s", frames[i%len(frames)], text)
fmt.Fprintf(w, "\r%s", line)
// primitive protection against very long lines
if len(line) > 2000 {
line = line[:2000]
}
i++
}
}
}()
return func() {
close(stop)
wg.Wait()
}
}
2 changes: 1 addition & 1 deletion internal/dsn/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (r *PostgreSQLResolver) Parse(dsn string) (*DSNInfo, error) {

// Detect scheme (postgres:// or postgresql://)
scheme := ""
remainder := dsn
var remainder string
if strings.HasPrefix(dsn, "postgresql://") {
scheme = "postgresql"
remainder = strings.TrimPrefix(dsn, "postgresql://")
Expand Down
2 changes: 1 addition & 1 deletion internal/manifest/fetcher_dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ package manifest

// defaultManifestURL is the localhost manifest URL for dev builds.
// Dev builds default to production URL, but this can be overridden with --backend-url flag.
const defaultManifestURL = "https://dashboard.seedfa.st/cli-endpoints.json"
const defaultManifestURL = "https://seedfa.st/cli-endpoints.json"
2 changes: 1 addition & 1 deletion internal/manifest/fetcher_prod.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
package manifest

// defaultManifestURL is the production manifest URL.
const defaultManifestURL = "https://dashboard.seedfa.st/cli-endpoints.json"
const defaultManifestURL = "https://seedfa.st/cli-endpoints.json"
Loading
Loading