Files
gastown/internal/cmd/dolt.go
mayor 2ee5e1c5ad feat(dolt): add gt dolt command for server management
Add `gt dolt` command with subcommands to manage the Dolt SQL server:
- start/stop/status: Control server lifecycle
- logs: View server logs
- sql: Open interactive SQL shell
- list: List available rig databases
- init-rig: Initialize new rig database
- migrate: Migrate from old .beads/dolt/ layout

The command detects both servers started via `gt dolt start` and
externally-started dolt processes by checking port 3307.

Closes: hq-05caeb

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 15:34:37 -08:00

395 lines
11 KiB
Go

package cmd
import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/doltserver"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/workspace"
)
var doltCmd = &cobra.Command{
Use: "dolt",
GroupID: GroupServices,
Short: "Manage the Dolt SQL server",
RunE: requireSubcommand,
Long: `Manage the Dolt SQL server for Gas Town beads.
The Dolt server provides multi-client access to all rig databases,
avoiding the single-writer limitation of embedded Dolt mode.
Server configuration:
- Port: 3307 (avoids conflict with MySQL on 3306)
- User: root (default Dolt user, no password for localhost)
- Data directory: .dolt-data/ (contains all rig databases)
Each rig (hq, gastown, beads) has its own database subdirectory.`,
}
var doltStartCmd = &cobra.Command{
Use: "start",
Short: "Start the Dolt server",
Long: `Start the Dolt SQL server in the background.
The server will run until stopped with 'gt dolt stop'.`,
RunE: runDoltStart,
}
var doltStopCmd = &cobra.Command{
Use: "stop",
Short: "Stop the Dolt server",
Long: `Stop the running Dolt SQL server.`,
RunE: runDoltStop,
}
var doltStatusCmd = &cobra.Command{
Use: "status",
Short: "Show Dolt server status",
Long: `Show the current status of the Dolt SQL server.`,
RunE: runDoltStatus,
}
var doltLogsCmd = &cobra.Command{
Use: "logs",
Short: "View Dolt server logs",
Long: `View the Dolt server log file.`,
RunE: runDoltLogs,
}
var doltSQLCmd = &cobra.Command{
Use: "sql",
Short: "Open Dolt SQL shell",
Long: `Open an interactive SQL shell to the Dolt database.
Works in both embedded mode (no server) and server mode.
For multi-client access, start the server first with 'gt dolt start'.`,
RunE: runDoltSQL,
}
var doltInitRigCmd = &cobra.Command{
Use: "init-rig <name>",
Short: "Initialize a new rig database",
Long: `Initialize a new rig database in the Dolt data directory.
Each rig (e.g., gastown, beads) gets its own database that will be
served by the Dolt server. The rig name becomes the database name
when connecting via MySQL protocol.
Example:
gt dolt init-rig gastown
gt dolt init-rig beads`,
Args: cobra.ExactArgs(1),
RunE: runDoltInitRig,
}
var doltListCmd = &cobra.Command{
Use: "list",
Short: "List available rig databases",
Long: `List all rig databases in the Dolt data directory.`,
RunE: runDoltList,
}
var doltMigrateCmd = &cobra.Command{
Use: "migrate",
Short: "Migrate existing dolt databases to centralized data directory",
Long: `Migrate existing dolt databases from .beads/dolt/ locations to the
centralized .dolt-data/ directory structure.
This command will:
1. Detect existing dolt databases in .beads/dolt/ directories
2. Move them to .dolt-data/<rigname>/
3. Remove the old empty directories
After migration, start the server with 'gt dolt start'.`,
RunE: runDoltMigrate,
}
var (
doltLogLines int
doltLogFollow bool
)
func init() {
doltCmd.AddCommand(doltStartCmd)
doltCmd.AddCommand(doltStopCmd)
doltCmd.AddCommand(doltStatusCmd)
doltCmd.AddCommand(doltLogsCmd)
doltCmd.AddCommand(doltSQLCmd)
doltCmd.AddCommand(doltInitRigCmd)
doltCmd.AddCommand(doltListCmd)
doltCmd.AddCommand(doltMigrateCmd)
doltLogsCmd.Flags().IntVarP(&doltLogLines, "lines", "n", 50, "Number of lines to show")
doltLogsCmd.Flags().BoolVarP(&doltLogFollow, "follow", "f", false, "Follow log output")
rootCmd.AddCommand(doltCmd)
}
func runDoltStart(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
if err := doltserver.Start(townRoot); err != nil {
return err
}
// Get state for display
state, _ := doltserver.LoadState(townRoot)
config := doltserver.DefaultConfig(townRoot)
fmt.Printf("%s Dolt server started (PID %d, port %d)\n",
style.Bold.Render("✓"), state.PID, config.Port)
fmt.Printf(" Data dir: %s\n", state.DataDir)
fmt.Printf(" Databases: %s\n", style.Dim.Render(strings.Join(state.Databases, ", ")))
fmt.Printf(" Connection: %s\n", style.Dim.Render(doltserver.GetConnectionString(townRoot)))
return nil
}
func runDoltStop(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
_, pid, _ := doltserver.IsRunning(townRoot)
if err := doltserver.Stop(townRoot); err != nil {
return err
}
fmt.Printf("%s Dolt server stopped (was PID %d)\n", style.Bold.Render("✓"), pid)
return nil
}
func runDoltStatus(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
running, pid, err := doltserver.IsRunning(townRoot)
if err != nil {
return fmt.Errorf("checking server status: %w", err)
}
config := doltserver.DefaultConfig(townRoot)
if running {
fmt.Printf("%s Dolt server is %s (PID %d)\n",
style.Bold.Render("●"),
style.Bold.Render("running"),
pid)
// Load state for more details
state, err := doltserver.LoadState(townRoot)
if err == nil && !state.StartedAt.IsZero() {
fmt.Printf(" Started: %s\n", state.StartedAt.Format("2006-01-02 15:04:05"))
fmt.Printf(" Port: %d\n", state.Port)
fmt.Printf(" Data dir: %s\n", state.DataDir)
if len(state.Databases) > 0 {
fmt.Printf(" Databases:\n")
for _, db := range state.Databases {
fmt.Printf(" - %s\n", db)
}
}
fmt.Printf(" Connection: %s\n", doltserver.GetConnectionString(townRoot))
}
} else {
fmt.Printf("%s Dolt server is %s\n",
style.Dim.Render("○"),
"not running")
// List available databases
databases, _ := doltserver.ListDatabases(townRoot)
if len(databases) == 0 {
fmt.Printf("\n%s No rig databases found in %s\n",
style.Bold.Render("!"),
config.DataDir)
fmt.Printf(" Initialize with: %s\n", style.Dim.Render("gt dolt init-rig <name>"))
} else {
fmt.Printf("\nAvailable databases in %s:\n", config.DataDir)
for _, db := range databases {
fmt.Printf(" - %s\n", db)
}
fmt.Printf("\nStart with: %s\n", style.Dim.Render("gt dolt start"))
}
}
return nil
}
func runDoltLogs(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
config := doltserver.DefaultConfig(townRoot)
if _, err := os.Stat(config.LogFile); os.IsNotExist(err) {
return fmt.Errorf("no log file found at %s", config.LogFile)
}
if doltLogFollow {
// Use tail -f for following
tailCmd := exec.Command("tail", "-f", config.LogFile)
tailCmd.Stdout = os.Stdout
tailCmd.Stderr = os.Stderr
return tailCmd.Run()
}
// Use tail -n for last N lines
tailCmd := exec.Command("tail", "-n", strconv.Itoa(doltLogLines), config.LogFile)
tailCmd.Stdout = os.Stdout
tailCmd.Stderr = os.Stderr
return tailCmd.Run()
}
func runDoltSQL(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
config := doltserver.DefaultConfig(townRoot)
// Check if server is running - if so, connect via Dolt SQL client
running, _, _ := doltserver.IsRunning(townRoot)
if running {
// Connect to running server using dolt sql client
// Using --no-tls since local server doesn't have TLS configured
sqlCmd := exec.Command("dolt",
"--host", "127.0.0.1",
"--port", strconv.Itoa(config.Port),
"--user", config.User,
"--password", "",
"--no-tls",
"sql",
)
sqlCmd.Stdin = os.Stdin
sqlCmd.Stdout = os.Stdout
sqlCmd.Stderr = os.Stderr
return sqlCmd.Run()
}
// Server not running - list databases and pick first one for embedded mode
databases, err := doltserver.ListDatabases(townRoot)
if err != nil {
return fmt.Errorf("listing databases: %w", err)
}
if len(databases) == 0 {
return fmt.Errorf("no databases found in %s\nInitialize with: gt dolt init-rig <name>", config.DataDir)
}
// Use first database for embedded SQL shell
dbDir := doltserver.RigDatabaseDir(townRoot, databases[0])
fmt.Printf("Using database: %s (start server with 'gt dolt start' for multi-database access)\n\n", databases[0])
sqlCmd := exec.Command("dolt", "sql")
sqlCmd.Dir = dbDir
sqlCmd.Stdin = os.Stdin
sqlCmd.Stdout = os.Stdout
sqlCmd.Stderr = os.Stderr
return sqlCmd.Run()
}
func runDoltInitRig(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
rigName := args[0]
if err := doltserver.InitRig(townRoot, rigName); err != nil {
return err
}
config := doltserver.DefaultConfig(townRoot)
rigDir := doltserver.RigDatabaseDir(townRoot, rigName)
fmt.Printf("%s Initialized rig database %q\n", style.Bold.Render("✓"), rigName)
fmt.Printf(" Location: %s\n", rigDir)
fmt.Printf(" Data dir: %s\n", config.DataDir)
fmt.Printf("\nStart server with: %s\n", style.Dim.Render("gt dolt start"))
return nil
}
func runDoltList(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
config := doltserver.DefaultConfig(townRoot)
databases, err := doltserver.ListDatabases(townRoot)
if err != nil {
return fmt.Errorf("listing databases: %w", err)
}
if len(databases) == 0 {
fmt.Printf("No rig databases found in %s\n", config.DataDir)
fmt.Printf("\nInitialize with: %s\n", style.Dim.Render("gt dolt init-rig <name>"))
return nil
}
fmt.Printf("Rig databases in %s:\n\n", config.DataDir)
for _, db := range databases {
dbDir := doltserver.RigDatabaseDir(townRoot, db)
fmt.Printf(" %s\n %s\n", style.Bold.Render(db), style.Dim.Render(dbDir))
}
return nil
}
func runDoltMigrate(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwdOrError()
if err != nil {
return fmt.Errorf("not in a Gas Town workspace: %w", err)
}
// Check if server is running - must stop first
running, _, _ := doltserver.IsRunning(townRoot)
if running {
return fmt.Errorf("Dolt server is running. Stop it first with: gt dolt stop")
}
// Find databases to migrate
migrations := doltserver.FindMigratableDatabases(townRoot)
if len(migrations) == 0 {
fmt.Println("No databases found to migrate.")
return nil
}
fmt.Printf("Found %d database(s) to migrate:\n\n", len(migrations))
for _, m := range migrations {
fmt.Printf(" %s\n", m.SourcePath)
fmt.Printf(" → %s\n\n", m.TargetPath)
}
// Perform migrations
for _, m := range migrations {
fmt.Printf("Migrating %s...\n", m.RigName)
if err := doltserver.MigrateRigFromBeads(townRoot, m.RigName, m.SourcePath); err != nil {
return fmt.Errorf("migrating %s: %w", m.RigName, err)
}
fmt.Printf(" %s Migrated to %s\n", style.Bold.Render("✓"), m.TargetPath)
}
fmt.Printf("\n%s Migration complete.\n", style.Bold.Render("✓"))
fmt.Printf("\nStart server with: %s\n", style.Dim.Render("gt dolt start"))
return nil
}