Skip to content
Closed
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
10 changes: 6 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version: '1.24'

- name: Download dependencies
run: go mod download
Expand All @@ -44,9 +44,11 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version: '1.24'

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
# v8+ is required: .golangci.yml uses the golangci-lint v2 config schema
# (version: "2", linters.settings, formatters), which v1 cannot parse.
uses: golangci/golangci-lint-action@v8
with:
version: latest
version: v2.12.2
67 changes: 67 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
version: "2"

run:
timeout: 8m

linters:
default: standard
enable:
# standard set (errcheck, govet, ineffassign, staticcheck, unused) plus:
- errorlint # error comparisons/assertions that break under wrapping
- nilerr # returning nil when a non-nil error is in hand
- gocritic # opinionated but high-signal style/perf diagnostics
- revive # doc comments, shadowed builtins, unused params
- unconvert # redundant type conversions
- misspell # typos in comments and strings
- bodyclose # unclosed HTTP response bodies

settings:
revive:
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
- name: increment-decrement
- name: var-naming
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: indent-error-flow
- name: errorf
- name: redefines-builtin-id
- name: unused-parameter
- name: unreachable-code
- name: superfluous-else

exclusions:
rules:
# Mock and fake implementations must match interface signatures, so their
# parameters are structurally required even when unused. Renaming them all
# to `_` would make the test doubles harder to read, not easier.
- path: _test\.go
linters:
- revive
text: "unused-parameter"

# stats.StatsResult, budget.BudgetEstimate and budget.BudgetSource trip
# revive's stutter check. Renaming an exported type is a cross-package API
# break (15 call sites across the repo) and does not belong in a lint-only
# change; each should be done deliberately in its own commit.
- path: (internal/stats/|internal/budget/)
linters:
- revive
text: "and that stutters"

formatters:
enable:
- gofmt

issues:
max-issues-per-linter: 0
max-same-issues: 0
2 changes: 1 addition & 1 deletion cmd/nightshift/commands/budget.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var budgetCmd = &cobra.Command{
Long: `Display current budget status and usage.

Shows spending across all providers or a specific provider.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
provider, _ := cmd.Flags().GetString("provider")
return runBudget(provider)
},
Expand Down
6 changes: 3 additions & 3 deletions cmd/nightshift/commands/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var configCmd = &cobra.Command{

Shows current configuration merged from global and project configs.
Use subcommands to get/set specific values or validate the config.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(_ *cobra.Command, _ []string) error {
return runConfigShow()
},
}
Expand All @@ -36,7 +36,7 @@ Examples:
nightshift config get providers.claude.enabled
nightshift config get logging.level`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(_ *cobra.Command, args []string) error {
return runConfigGet(args[0])
},
}
Expand Down Expand Up @@ -66,7 +66,7 @@ var configValidateCmd = &cobra.Command{
Long: `Validate the current configuration.

Checks both global and project configs for errors.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(_ *cobra.Command, _ []string) error {
return runConfigValidate()
},
}
Expand Down
25 changes: 14 additions & 11 deletions cmd/nightshift/commands/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package commands

import (
"context"
"errors"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -129,7 +130,7 @@ func isDaemonRunning() (bool, int) {
return isProcessRunning(pid), pid
}

func runDaemonStart(cmd *cobra.Command, args []string) error {
func runDaemonStart(_ *cobra.Command, _ []string) error {
// Check if already running
if running, pid := isDaemonRunning(); running {
return fmt.Errorf("daemon already running (pid %d)", pid)
Expand Down Expand Up @@ -238,7 +239,7 @@ func runDaemonLoop(cfg *config.Config) error {
<-ctx.Done()

// Stop scheduler gracefully
if err := sched.Stop(); err != nil && err != scheduler.ErrNotRunning {
if err := sched.Stop(); err != nil && !errors.Is(err, scheduler.ErrNotRunning) {
log.Errorf("stopping scheduler: %v", err)
}

Expand Down Expand Up @@ -564,28 +565,30 @@ func takeSnapshot(ctx context.Context, cfg *config.Config, database *db.DB, log

if cfg.Providers.Claude.Enabled {
snapshot, err := collector.TakeSnapshot(ctx, "claude")
if err != nil {
switch {
case err != nil:
log.Warnf("snapshot claude: %v", err)
} else if snapshot.ScrapedPct != nil {
case snapshot.ScrapedPct != nil:
log.Infof("snapshot claude: %.1f%%", *snapshot.ScrapedPct)
} else {
default:
log.Info("snapshot claude: local-only")
}
}

if cfg.Providers.Codex.Enabled {
snapshot, err := collector.TakeSnapshot(ctx, "codex")
if err != nil {
switch {
case err != nil:
log.Warnf("snapshot codex: %v", err)
} else if snapshot.ScrapedPct != nil {
case snapshot.ScrapedPct != nil:
log.Infof("snapshot codex: %.1f%%", *snapshot.ScrapedPct)
} else {
default:
log.Info("snapshot codex: local-only")
}
}
}

func pruneSnapshots(ctx context.Context, cfg *config.Config, database *db.DB, log *logging.Logger) {
func pruneSnapshots(_ context.Context, cfg *config.Config, database *db.DB, log *logging.Logger) {
collector := snapshots.NewCollector(database, nil, nil, nil, nil, weekStartDayFromConfig(cfg))
deleted, err := collector.Prune(cfg.Budget.SnapshotRetentionDays)
if err != nil {
Expand All @@ -609,7 +612,7 @@ func weekStartDayFromConfig(cfg *config.Config) time.Weekday {
}
}

func runDaemonStop(cmd *cobra.Command, args []string) error {
func runDaemonStop(_ *cobra.Command, _ []string) error {
running, pid := isDaemonRunning()
if !running {
// Check if PID file exists but process is dead
Expand Down Expand Up @@ -657,7 +660,7 @@ func runDaemonStop(cmd *cobra.Command, args []string) error {
}
}

func runDaemonStatus(cmd *cobra.Command, args []string) error {
func runDaemonStatus(_ *cobra.Command, _ []string) error {
running, pid := isDaemonRunning()

if !running {
Expand Down
2 changes: 1 addition & 1 deletion cmd/nightshift/commands/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func init() {
rootCmd.AddCommand(doctorCmd)
}

func runDoctor(cmd *cobra.Command, args []string) error {
func runDoctor(_ *cobra.Command, _ []string) error {
// Augment PATH the same way 'run' does so CLI checks are accurate.
ensurePATH()

Expand Down
2 changes: 1 addition & 1 deletion cmd/nightshift/commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func init() {
rootCmd.AddCommand(initCmd)
}

func runInit(cmd *cobra.Command, args []string) error {
func runInit(cmd *cobra.Command, _ []string) error {
global, _ := cmd.Flags().GetBool("global")
force, _ := cmd.Flags().GetBool("force")

Expand Down
4 changes: 2 additions & 2 deletions cmd/nightshift/commands/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func init() {
}

// runInstall implements the install command
func runInstall(cmd *cobra.Command, args []string) error {
func runInstall(_ *cobra.Command, args []string) error {
// Determine service type
serviceType := ""
if len(args) > 0 {
Expand Down Expand Up @@ -378,7 +378,7 @@ func installCron(binaryPath string, cfg *config.Config) error {
}

// runUninstall implements the uninstall command
func runUninstall(cmd *cobra.Command, args []string) error {
func runUninstall(_ *cobra.Command, _ []string) error {
var errors []string
removed := false

Expand Down
2 changes: 1 addition & 1 deletion cmd/nightshift/commands/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var logsCmd = &cobra.Command{
Long: `View nightshift logs.

Displays recent log entries. Use --follow to stream logs in real-time.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
tail, _ := cmd.Flags().GetInt("tail")
follow, _ := cmd.Flags().GetBool("follow")
export, _ := cmd.Flags().GetString("export")
Expand Down
2 changes: 1 addition & 1 deletion cmd/nightshift/commands/preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func init() {
rootCmd.AddCommand(previewCmd)
}

func runPreview(cmd *cobra.Command, args []string) error {
func runPreview(cmd *cobra.Command, _ []string) error {
runs, _ := cmd.Flags().GetInt("runs")
projectPath, _ := cmd.Flags().GetString("project")
taskFilter, _ := cmd.Flags().GetString("task")
Expand Down
9 changes: 5 additions & 4 deletions cmd/nightshift/commands/preview_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,12 @@ func renderPreviewText(result *previewResult, opts previewTextOptions) string {
fmt.Fprintln(b, line)
}
}
if result.TaskFilter != "" {
switch {
case result.TaskFilter != "":
fmt.Fprintf(b, " Task filter: %s\n", result.TaskFilter)
} else if len(result.EnabledTasks) == 0 {
case len(result.EnabledTasks) == 0:
b.WriteString(" Task filter: all enabled tasks (none explicitly enabled)\n")
} else {
default:
fmt.Fprintf(b, " Task filter: enabled list (%d) [%s]\n", len(result.EnabledTasks), strings.Join(result.EnabledTasks, ", "))
}
if opts.Explain && result.ProjectCount > 1 {
Expand Down Expand Up @@ -347,7 +348,7 @@ func renderDiagnosticsText(b *strings.Builder, styles previewStyles, diagnostics
renderCooldownsText(b, styles, diagnostics.Cooldowns, indent)
}

func renderCooldownsText(b *strings.Builder, styles previewStyles, cooldowns []previewCooldownEntry, indent string) {
func renderCooldownsText(b *strings.Builder, _ previewStyles, cooldowns []previewCooldownEntry, indent string) {
if len(cooldowns) == 0 {
return
}
Expand Down
9 changes: 5 additions & 4 deletions cmd/nightshift/commands/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ var reportCmd = &cobra.Command{
Long: `View structured reports from recent nightshift runs.

By default, shows a polished overview of what happened during the last night.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
opts := reportOptions{}
opts.reportType, _ = cmd.Flags().GetString("report")
opts.period, _ = cmd.Flags().GetString("period")
Expand Down Expand Up @@ -895,16 +895,17 @@ func renderReportBudget(styles reportStyles, runs []reportRun) string {
b.WriteString(styles.Accent.Render(header))
b.WriteString("\n")

if summary.BudgetStart > 0 {
switch {
case summary.BudgetStart > 0:
fmt.Fprintf(&b, " %s %s used / %s start (%s remaining)\n",
styles.Label.Render("Budget:"),
formatTokensCompact(summary.TokensUsed),
formatTokensCompact(summary.BudgetStart),
formatTokensCompact(summary.BudgetRemaining),
)
} else if summary.TokensUsed > 0 {
case summary.TokensUsed > 0:
fmt.Fprintf(&b, " %s %s\n", styles.Label.Render("Tokens:"), formatTokensCompact(summary.TokensUsed))
} else {
default:
b.WriteString(" No budget data recorded\n")
}

Expand Down
9 changes: 5 additions & 4 deletions cmd/nightshift/commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func init() {
rootCmd.AddCommand(runCmd)
}

func runRun(cmd *cobra.Command, args []string) error {
func runRun(cmd *cobra.Command, _ []string) error {
dryRun, _ := cmd.Flags().GetBool("dry-run")
projectPath, _ := cmd.Flags().GetString("project")
taskFilter, _ := cmd.Flags().GetString("task")
Expand Down Expand Up @@ -465,7 +465,8 @@ func buildPreflight(p executeRunParams) (*preflightPlan, error) {
// Select tasks
var selectedTasks []tasks.ScoredTask

if p.taskFilter != "" {
switch {
case p.taskFilter != "":
def, err := tasks.GetDefinition(tasks.TaskType(p.taskFilter))
if err != nil {
return nil, fmt.Errorf("unknown task type: %s", p.taskFilter)
Expand All @@ -475,15 +476,15 @@ func buildPreflight(p executeRunParams) (*preflightPlan, error) {
Score: p.selector.ScoreTask(def.Type, projectPath),
Project: projectPath,
}}
} else if p.randomTask {
case p.randomTask:
taskBudget := choice.allowance.Allowance
if p.ignoreBudget {
taskBudget = math.MaxInt64
}
if picked := p.selector.SelectRandom(taskBudget, projectPath); picked != nil {
selectedTasks = []tasks.ScoredTask{*picked}
}
} else {
default:
n := p.maxTasks
if n <= 0 {
n = 1
Expand Down
13 changes: 7 additions & 6 deletions cmd/nightshift/commands/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var setupCmd = &cobra.Command{

Creates/updates the global config, validates providers, runs a snapshot, previews the next run,
and optionally installs/enables the daemon.`,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(_ *cobra.Command, _ []string) error {
model, err := newSetupModel()
if err != nil {
return err
Expand Down Expand Up @@ -1507,11 +1507,12 @@ func renderEnvChecks(cfg *config.Config) string {
// Check for Copilot CLI (gh or copilot binary)
_, ghErr := execLookPath("gh")
_, copilotErr := execLookPath("copilot")
if ghErr != nil && copilotErr != nil {
switch {
case ghErr != nil && copilotErr != nil:
fmt.Fprintf(&b, " %s %s\n", styleWarn.Render("Note:"), "Copilot CLI not found (install via 'gh' or native 'copilot')")
} else if ghErr == nil {
case ghErr == nil:
fmt.Fprintf(&b, " %s %s\n", styleOk.Render("OK:"), "gh CLI available (use 'gh copilot')")
} else {
default:
fmt.Fprintf(&b, " %s %s\n", styleOk.Render("OK:"), "copilot CLI available")
}
if cfg.Providers.Claude.Enabled {
Expand Down Expand Up @@ -1910,11 +1911,11 @@ func uninstallService(service string) error {

func mustExecutablePath() string {
path, _ := os.Executable()
real, err := filepath.EvalSymlinks(path)
resolved, err := filepath.EvalSymlinks(path)
if err != nil {
return path
}
return real
return resolved
}

func writeGlobalConfig(cfg *config.Config) error {
Expand Down
Loading