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>
395 lines
11 KiB
Go
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
|
|
}
|