Skip to content

feat(cli): mantle init connection recovery & Docker auto-provisioning (#7)#19

Merged
michaelmcnees merged 15 commits into
mainfrom
feat/init-connection-recovery
Mar 25, 2026
Merged

feat(cli): mantle init connection recovery & Docker auto-provisioning (#7)#19
michaelmcnees merged 15 commits into
mainfrom
feat/init-connection-recovery

Conversation

@michaelmcnees

@michaelmcnees michaelmcnees commented Mar 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • mantle init now gracefully handles missing Postgres instead of failing with a connection error
  • Loopback host failure → offers to start a Docker Postgres container automatically, matching default config
  • Remote host failure → shows diagnostic error, offers retry (re-reads config) or quit
  • Non-interactive mode (piped stdin, CI) → returns error immediately, no prompts
  • Extracts duplicated constants into shared packages: netutil.IsLoopback, dbdefaults, budget.ResetMode*
  • Updates landing page quickstart and getting-started docs to remove docker compose up -d step

Closes #7

Constant extraction

Package Constants Deduped from
internal/netutil IsLoopback() config.go SSL warning + new init.go
internal/dbdefaults PostgresImage, User, Password, Database, TestDatabase, ContainerName 7 test files + docker.go
internal/budget ResetModeCalendar, ResetModeRolling config.go + budget.go

Connection recovery flow

mantle init
  ├─ db.Open succeeds → run migrations → done
  └─ db.Open fails
       ├─ non-loopback host → error + [R]etry / [Q]uit
       └─ loopback host → offer Docker auto-provisioning
            ├─ user accepts + Docker available → start container → migrations
            ├─ user accepts + no Docker → [R]etry / [C]onnection string
            └─ user declines → [R]etry / [C]onnection string

Test plan

  • netutil.IsLoopback — 9 cases (localhost variants, IPv4/6, remote hosts, empty)
  • dockerRunArgs — verifies exact arg slice matches defaults
  • parseHostFromURL — 6 cases (standard, remote, IPv4, IPv6, no-port, empty)
  • isInteractive — returns false in test/CI context
  • handleConnectionFailure non-interactive — returns error immediately, no stdout
  • All 7 testcontainers test files pass with dbdefaults constants
  • Budget + config tests pass with ResetMode* constants
  • go vet ./... clean
  • Site builds successfully

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Automatic local Postgres provisioning via Docker when mantle init detects an unreachable database connection
    • Interactive recovery flow for connection failures with intelligent fallback options based on host type
  • Documentation

    • Simplified getting-started guide; Docker Compose no longer required
    • Docker is now optional and invoked automatically when needed for local database setup
  • Chores

    • Consolidated database configuration defaults across test suites for consistency

michaelmcnees and others added 13 commits March 24, 2026 09:28
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Users can fix typos, firewall rules, or start their DB without
restarting mantle init.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract duplicated testcontainers defaults (dbdefaults), loopback
detection (netutil), and budget reset modes into shared packages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10-task plan covering constant extraction (netutil, dbdefaults, budget
modes), Docker auto-provisioning, interactive recovery flow, and
quickstart doc updates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hard-coded "postgres:16-alpine", "mantle_test", "mantle"/"mantle"
string literals in all 7 test files with dbdefaults.PostgresImage,
dbdefaults.TestDatabase, dbdefaults.User, and dbdefaults.Password.
The connector/postgres_test.go retains its intentionally distinct
ext_test/testuser/testpass credentials and only adopts the image constant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds dockerRunArgs, parseHostFromURL, dockerAvailable, dockerContainerStatus,
dockerRemoveContainer, dockerStartPostgres, and waitForPostgres — the building
blocks for automatic Postgres container provisioning when init detects the
database isn't reachable on localhost.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When db.Open fails, classify the host as loopback or remote and offer
interactive recovery: Docker auto-provisioning for localhost, retry/quit
with config reload for remote hosts. Non-interactive (CI/piped) mode
returns the error immediately.

Split isInteractive() into platform-specific files using TIOCGETA/TCGETS
ioctl instead of ModeCharDevice, so go test on macOS (where /dev/null
registers as a char device) correctly reports non-interactive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@michaelmcnees has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 37 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ba5b5ce2-950f-47b5-95de-6568d5f688e2

📥 Commits

Reviewing files that changed from the base of the PR and between 4a13a4d and 750520d.

📒 Files selected for processing (4)
  • internal/cli/docker.go
  • internal/cli/init.go
  • internal/config/config.go
  • internal/netutil/loopback_test.go
📝 Walkthrough

Walkthrough

This pull request implements automatic connection recovery for mantle init when the configured Postgres database is unreachable. It introduces host classification logic, centralizes database constants, adds Docker container lifecycle management, and branches the init flow based on whether the target host is local (loopback) or remote, offering appropriate recovery options.

Changes

Cohort / File(s) Summary
New Loopback Classification
internal/netutil/loopback.go, internal/netutil/loopback_test.go
Adds IsLoopback(host string) bool utility to classify localhost addresses (including localhost, 127.0.0.1, ::1) via case-insensitive comparison and IP parsing.
Shared Database Defaults
internal/dbdefaults/dbdefaults.go
New constants package centralizing Postgres image (postgres:16-alpine), credentials (mantle/mantle), container name, and test database name across runtime and test contexts.
Docker Container Lifecycle
internal/cli/docker.go, internal/cli/docker_test.go
Docker helper utilities for running arguments construction, container status inspection, removal, and a dockerStartPostgres flow that starts a container and polls for readiness via retried db.Open calls with exponential backoff (15s timeout).
Interactive Context Detection
internal/cli/interactive.go
Utility function isInteractive() that detects whether stdin is attached to a TTY using golang.org/x/term.
Init Connection Recovery
internal/cli/init.go, internal/cli/init_test.go
Enhanced init command with handleConnectionFailure routing: non-interactive mode returns error; loopback failures prompt for Docker provisioning or manual connection string entry with validation; remote failures show retry/quit loop with config reload on each retry attempt.
Config & Budget Refactoring
internal/config/config.go, internal/budget/budget.go
Config now uses netutil.IsLoopback for SSL warning checks instead of inline logic; budget exports ResetModeCalendar and ResetModeRolling constants replacing string literals.
Test Suite Updates
internal/auth/auth_test.go, internal/budget/store_test.go, internal/connector/postgres_test.go, internal/db/migrate_test.go, internal/engine/test_helpers_test.go, internal/secret/store_test.go, internal/workflow/store_test.go
Refactored Postgres testcontainer setup across multiple test files to use dbdefaults constants instead of hardcoded image tags and credentials.
Documentation
site/src/components/GetStarted.astro, site/src/content/docs/getting-started/index.md
Updated quickstart/getting-started guides: removed Docker Compose requirement, simplified to go install + mantle init, noted Docker is optional for automatic local Postgres provisioning, and clarified remote database setup via MANTLE_DATABASE_URL.
Implementation Plans & Specs
docs/superpowers/plans/2026-03-24-init-connection-recovery.md, docs/superpowers/specs/2026-03-24-init-connection-recovery-design.md
Comprehensive design and implementation documentation for the connection recovery feature, including host classification, Docker provisioning, interactive prompts, and test strategy.
Dependency Update
go.mod
Added indirect dependency golang.org/x/term v0.41.0 for TTY detection.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as mantle init
    participant Config as Config Loader
    participant DB as db.Open()
    participant Detector as netutil.IsLoopback
    participant Docker as Docker Engine
    participant Postgres as Postgres Container
    
    User->>CLI: Run `mantle init`
    CLI->>Config: Load database config
    Config-->>CLI: Return config with DB URL
    CLI->>DB: Attempt db.Open()
    
    alt Connection Success
        DB-->>CLI: Connected
        CLI->>User: Proceed to migrations
    else Connection Failed
        DB-->>CLI: Connection error
        CLI->>Detector: Classify host (loopback?)
        
        alt Loopback/Localhost
            Detector-->>CLI: true (loopback)
            alt Interactive Mode
                CLI->>User: Prompt: Start Postgres via Docker?
                User-->>CLI: Yes
                CLI->>Docker: Check docker available
                Docker-->>CLI: Docker ready
                CLI->>Docker: Start postgres:16-alpine container
                Docker->>Postgres: Spawn container
                Postgres-->>Docker: Container running
                Docker-->>CLI: Container started
                CLI->>Postgres: Poll db.Open() with backoff (15s timeout)
                alt Postgres Ready
                    Postgres-->>CLI: Accept connection
                    CLI->>DB: db.Open() succeeds
                    DB-->>CLI: Connected
                    CLI->>User: Proceed to migrations
                else Postgres Timeout
                    Postgres-->>CLI: Timeout after 15s
                    CLI->>User: Prompt: Enter connection string or retry
                end
            else Non-Interactive
                CLI-->>User: Return connection error
            end
        else Remote Host
            Detector-->>CLI: false (remote)
            alt Interactive Mode
                loop Retry Loop
                    CLI->>User: Show error, offer [R]etry or [Q]uit
                    User-->>CLI: Retry
                    CLI->>Config: Reload config
                    Config-->>CLI: Updated config
                    CLI->>DB: Attempt db.open()
                    DB-->>CLI: Connection result
                end
            else Non-Interactive
                CLI-->>User: Return connection error
            end
        end
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly Related PRs

Poem

🐰 A container awakens at init's gentle call,
Docker spins up Postgres—no more auth sprawl,
When localhost whispers, we answer with care,
Connection restored! Let the migrations run fair.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding connection recovery and Docker auto-provisioning to the mantle init command.
Linked Issues check ✅ Passed The PR fully addresses issue #7 objectives: removes incorrect docker-compose quickstart step, implements loopback detection with interactive prompts, adds Docker auto-provisioning for local Postgres, and handles non-interactive mode with direct error return.
Out of Scope Changes check ✅ Passed All changes align with issue #7 objectives. While the PR includes refactorings (dbdefaults, netutil, budget constants) that support the core feature, these are necessary enablers for the connection recovery logic and documentation updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/init-connection-recovery

Comment @coderabbitai help to get the list of available commands and usage tips.

Gosec G103 flagged the raw ioctl calls. Use golang.org/x/term which
handles platform-specific TTY detection internally, and consolidate
three platform files into one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/cli/docker.go`:
- Around line 67-73: The switch in dockerContainerStatus() handling misses the
"paused" state causing a later docker run to fail; update the switch in the
function that calls dockerContainerStatus() to include "paused" alongside
"exited", "created", and "dead" so it calls dockerRemoveContainer() (or
alternatively add logic to call dockerUnpauseContainer() for "paused" before
proceeding), ensuring the code still returns waitForPostgres(cfg) when status ==
"running"; reference the dockerContainerStatus(), dockerRemoveContainer(), and
waitForPostgres(cfg) symbols when making the change.

In `@internal/cli/init.go`:
- Around line 131-150: The connection-prompt loop in init.go has no user-escape,
so update the loop after trimming connStr to accept a simple cancel/back token
(e.g. "q", "quit", "back") and handle it by printing an abort message and
returning early with a clear cancellation error instead of looping forever; keep
the rest of the flow that sets cfg.Database.URL and calls db.Open unchanged (use
the same cfg.Database and db.Open symbols) so callers can detect the user-cancel
condition.

In `@internal/dbdefaults/dbdefaults.go`:
- Around line 1-16: Update config.go to stop hardcoding the default DB URL and
instead build it from the dbdefaults package constants: import
internal/dbdefaults and fmt, construct the default URL using dbdefaults.User,
dbdefaults.Password and dbdefaults.Database (e.g. via
fmt.Sprintf("postgres://%s:%s@localhost:5432/%s?sslmode=prefer", ...)) and pass
that string to v.SetDefault("database.url", defaultURL) so the values in
dbdefaults (PostgresImage, User, Password, Database, TestDatabase) become the
single source of truth.

In `@internal/netutil/loopback_test.go`:
- Around line 10-29: Update the TestIsLoopback table-driven test to (1) include
an additional case for "127.0.0.2" with expected true to validate the full
127.0.0.0/8 range is treated as loopback by netutil.IsLoopback, and (2) add an
explicit name field to the test-case struct (e.g., name string) and use that
name in t.Run instead of tt.host so the empty-string case gets a readable
subtest name; ensure you update TestIsLoopback to reference the new name for
t.Run while keeping the host and expected fields as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2ee1a7e3-9fc3-4b2c-b667-a88d7cf136b3

📥 Commits

Reviewing files that changed from the base of the PR and between 957996c and 94c45bb.

📒 Files selected for processing (23)
  • docs/superpowers/plans/2026-03-24-init-connection-recovery.md
  • docs/superpowers/specs/2026-03-24-init-connection-recovery-design.md
  • internal/auth/auth_test.go
  • internal/budget/budget.go
  • internal/budget/store_test.go
  • internal/cli/docker.go
  • internal/cli/docker_test.go
  • internal/cli/init.go
  • internal/cli/init_test.go
  • internal/cli/interactive_darwin.go
  • internal/cli/interactive_linux.go
  • internal/cli/interactive_windows.go
  • internal/config/config.go
  • internal/connector/postgres_test.go
  • internal/db/migrate_test.go
  • internal/dbdefaults/dbdefaults.go
  • internal/engine/test_helpers_test.go
  • internal/netutil/loopback.go
  • internal/netutil/loopback_test.go
  • internal/secret/store_test.go
  • internal/workflow/store_test.go
  • site/src/components/GetStarted.astro
  • site/src/content/docs/getting-started/index.md

Comment thread internal/cli/docker.go
Comment thread internal/cli/init.go
Comment on lines +1 to +16
package dbdefaults

// Runtime defaults — used by Docker auto-provisioning and config defaults.
// These match the default database URL in config.go.
const (
PostgresImage = "postgres:16-alpine"
User = "mantle"
Password = "mantle"
Database = "mantle"
ContainerName = "mantle-postgres"
)

// Test defaults — used by testcontainers setups.
const (
TestDatabase = "mantle_test"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Good consolidation of defaults.

This centralizes the Postgres/Docker defaults effectively. The comment on line 4 notes these match config.go, but config.go still hardcodes the same values (see context snippet at line 145: "postgres://mantle:mantle@localhost:5432/mantle?sslmode=prefer").

Consider updating config.go to construct the default URL from these constants to ensure a true single source of truth:

// In config.go
import "github.com/dvflw/mantle/internal/dbdefaults"

defaultURL := fmt.Sprintf("postgres://%s:%s@localhost:5432/%s?sslmode=prefer",
    dbdefaults.User, dbdefaults.Password, dbdefaults.Database)
v.SetDefault("database.url", defaultURL)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/dbdefaults/dbdefaults.go` around lines 1 - 16, Update config.go to
stop hardcoding the default DB URL and instead build it from the dbdefaults
package constants: import internal/dbdefaults and fmt, construct the default URL
using dbdefaults.User, dbdefaults.Password and dbdefaults.Database (e.g. via
fmt.Sprintf("postgres://%s:%s@localhost:5432/%s?sslmode=prefer", ...)) and pass
that string to v.SetDefault("database.url", defaultURL) so the values in
dbdefaults (PostgresImage, User, Password, Database, TestDatabase) become the
single source of truth.

Comment thread internal/netutil/loopback_test.go
… config, test names

- docker.go: handle "paused" container state in dockerStartPostgres
- init.go: add q/quit/back escape from connection string prompt
- config.go: build default DB URL from dbdefaults constants
- loopback_test.go: add 127.0.0.2 case and named subtests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@michaelmcnees michaelmcnees merged commit 236e14f into main Mar 25, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Get Running in 5 Minutes Issue

1 participant