feat(cli): mantle init connection recovery & Docker auto-provisioning (#7)#19
Conversation
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>
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis pull request implements automatic connection recovery for Changes
Sequence DiagramsequenceDiagram
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
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~28 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
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>
There was a problem hiding this comment.
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
📒 Files selected for processing (23)
docs/superpowers/plans/2026-03-24-init-connection-recovery.mddocs/superpowers/specs/2026-03-24-init-connection-recovery-design.mdinternal/auth/auth_test.gointernal/budget/budget.gointernal/budget/store_test.gointernal/cli/docker.gointernal/cli/docker_test.gointernal/cli/init.gointernal/cli/init_test.gointernal/cli/interactive_darwin.gointernal/cli/interactive_linux.gointernal/cli/interactive_windows.gointernal/config/config.gointernal/connector/postgres_test.gointernal/db/migrate_test.gointernal/dbdefaults/dbdefaults.gointernal/engine/test_helpers_test.gointernal/netutil/loopback.gointernal/netutil/loopback_test.gointernal/secret/store_test.gointernal/workflow/store_test.gosite/src/components/GetStarted.astrosite/src/content/docs/getting-started/index.md
| 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" | ||
| ) |
There was a problem hiding this comment.
🧹 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.
… 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>
Summary
mantle initnow gracefully handles missing Postgres instead of failing with a connection errornetutil.IsLoopback,dbdefaults,budget.ResetMode*docker compose up -dstepCloses #7
Constant extraction
internal/netutilIsLoopback()internal/dbdefaultsPostgresImage,User,Password,Database,TestDatabase,ContainerNameinternal/budgetResetModeCalendar,ResetModeRollingConnection recovery flow
Test plan
netutil.IsLoopback— 9 cases (localhost variants, IPv4/6, remote hosts, empty)dockerRunArgs— verifies exact arg slice matches defaultsparseHostFromURL— 6 cases (standard, remote, IPv4, IPv6, no-port, empty)isInteractive— returns false in test/CI contexthandleConnectionFailurenon-interactive — returns error immediately, no stdoutdbdefaultsconstantsResetMode*constantsgo vet ./...clean🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
mantle initdetects an unreachable database connectionDocumentation
Chores