Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .infer/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
logs/*.log
history
chat_export_*
conversations.db
conversations.db*
conversations
bin/
tmp/
15 changes: 14 additions & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ This is the recommended command to start working with Inference Gateway CLI in a
func init() {
initCmd.Flags().Bool("overwrite", false, "Overwrite existing files if they already exist")
initCmd.Flags().Bool("userspace", false, "Initialize configuration in user home directory (~/.infer/)")
initCmd.Flags().Bool("skip-migrations", false, "Skip running database migrations")
rootCmd.AddCommand(initCmd)
}

func initializeProject(cmd *cobra.Command) error {
overwrite, _ := cmd.Flags().GetBool("overwrite")
userspace, _ := cmd.Flags().GetBool("userspace")
skipMigrations, _ := cmd.Flags().GetBool("skip-migrations")

var configPath, gitignorePath, scmShortcutsPath, gitShortcutsPath, mcpShortcutsPath, shellsShortcutsPath, exportShortcutsPath, a2aShortcutsPath, mcpPath string

Expand Down Expand Up @@ -79,7 +81,7 @@ func initializeProject(cmd *cobra.Command) error {
logs/*.log
history
chat_export_*
conversations.db
conversations.db*
conversations
bin/
tmp/
Expand Down Expand Up @@ -148,6 +150,17 @@ tmp/
fmt.Println("")
fmt.Println("Tip: Use /init in chat mode to generate an AGENTS.md file interactively")

if !skipMigrations {
fmt.Println("")
fmt.Println("Running database migrations...")
if err := runMigrations(); err != nil {
fmt.Printf("%s Warning: Failed to run migrations: %v\n", icons.CrossMarkStyle.Render(icons.CrossMark), err)
fmt.Println(" You can run migrations manually with: infer migrate")
} else {
fmt.Printf("%s Database migrations completed successfully\n", icons.CheckMarkStyle.Render(icons.CheckMark))
}
}

return nil
}

Expand Down
10 changes: 6 additions & 4 deletions cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ func TestInitializeProject(t *testing.T) {
{
name: "basic project initialization",
flags: map[string]any{
"overwrite": false,
"userspace": false,
"overwrite": false,
"userspace": false,
"skip-migrations": true,
},
wantFiles: []string{".infer/config.yaml", ".infer/.gitignore"},
wantNoFiles: []string{"AGENTS.md"},
Expand All @@ -30,8 +31,9 @@ func TestInitializeProject(t *testing.T) {
{
name: "userspace initialization",
flags: map[string]any{
"overwrite": true,
"userspace": true,
"overwrite": true,
"userspace": true,
"skip-migrations": true,
},
wantFiles: []string{},
wantNoFiles: []string{".infer/config.yaml", ".infer/.gitignore", "AGENTS.md"},
Expand Down
176 changes: 176 additions & 0 deletions cmd/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package cmd

import (
"context"
"fmt"
"os"

container "github.com/inference-gateway/cli/internal/container"
storage "github.com/inference-gateway/cli/internal/infra/storage"
migrations "github.com/inference-gateway/cli/internal/infra/storage/migrations"
icons "github.com/inference-gateway/cli/internal/ui/styles/icons"
cobra "github.com/spf13/cobra"
)

var migrateCmd = &cobra.Command{
Use: "migrate",
Short: "Run database migrations",
Long: `Run database migrations to update the schema to the latest version.

This command applies any pending migrations to your database. Migrations are tracked
in the schema_migrations table to ensure they are only applied once.

The command automatically detects your database backend (SQLite, PostgreSQL, JSONL, Redis, Memory)
and applies the appropriate migrations. Note that JSONL, Redis, and Memory storage backends
do not require migrations as they do not use a relational schema.`,
RunE: func(cmd *cobra.Command, args []string) error {
status, _ := cmd.Flags().GetBool("status")
if status {
return showMigrationStatus()
}
return runMigrations()
},
}

func init() {
migrateCmd.Flags().Bool("status", false, "Show migration status without applying migrations")
rootCmd.AddCommand(migrateCmd)
}

// runMigrations executes pending database migrations
func runMigrations() error {
cfg, err := getConfigFromViper()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}

serviceContainer := container.NewServiceContainer(cfg, V)

conversationStorage := serviceContainer.GetStorage()

defer func() {
if err := conversationStorage.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close storage: %v\n", err)
}
}()

switch conversationStorage.(type) {
case *storage.SQLiteStorage:
fmt.Printf("%s SQLite database migrations are up to date\n", icons.CheckMarkStyle.Render(icons.CheckMark))
fmt.Println(" All migrations have been applied automatically")
return nil
case *storage.PostgresStorage:
fmt.Printf("%s PostgreSQL database migrations are up to date\n", icons.CheckMarkStyle.Render(icons.CheckMark))
fmt.Println(" All migrations have been applied automatically")
return nil
case *storage.JsonlStorage:
fmt.Println("JSONL storage does not require migrations")
return nil
case *storage.MemoryStorage:
fmt.Println("Memory storage does not require migrations")
return nil
case *storage.RedisStorage:
fmt.Println("Redis storage does not require migrations")
return nil
default:
return fmt.Errorf("unsupported storage backend: %T", conversationStorage)
}
}

// showMigrationStatus displays the current migration status
func showMigrationStatus() error {
cfg, err := getConfigFromViper()
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}

serviceContainer := container.NewServiceContainer(cfg, V)

conversationStorage := serviceContainer.GetStorage()

defer func() {
if err := conversationStorage.Close(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to close storage: %v\n", err)
}
}()

switch s := conversationStorage.(type) {
case *storage.SQLiteStorage:
return showSQLiteMigrationStatus(s)
case *storage.PostgresStorage:
return showPostgresMigrationStatus(s)
case *storage.JsonlStorage:
fmt.Println("JSONL storage does not require migrations")
return nil
case *storage.MemoryStorage:
fmt.Println("Memory storage does not require migrations")
return nil
case *storage.RedisStorage:
fmt.Println("Redis storage does not require migrations")
return nil
default:
return fmt.Errorf("unsupported storage backend: %T", s)
}
}

// showSQLiteMigrationStatus shows migration status for SQLite
func showSQLiteMigrationStatus(s *storage.SQLiteStorage) error {
ctx := context.Background()
db := s.DB()
if db == nil {
return fmt.Errorf("database connection is nil")
}

runner := migrations.NewMigrationRunner(db, "sqlite")
allMigrations := migrations.GetSQLiteMigrations()

status, err := runner.GetMigrationStatus(ctx, allMigrations)
if err != nil {
return fmt.Errorf("failed to get migration status: %w", err)
}

fmt.Println("SQLite Migration Status:")
fmt.Println()
for _, s := range status {
statusIcon := icons.StyledCrossMark()
statusText := "Pending"
if s.Applied {
statusIcon = icons.StyledCheckMark()
statusText = "Applied"
}
fmt.Printf(" %s Version %s: %s (%s)\n", statusIcon, s.Version, s.Description, statusText)
}

return nil
}

// showPostgresMigrationStatus shows migration status for PostgreSQL
func showPostgresMigrationStatus(s *storage.PostgresStorage) error {
ctx := context.Background()
db := s.DB()
if db == nil {
return fmt.Errorf("database connection is nil")
}

runner := migrations.NewMigrationRunner(db, "postgres")
allMigrations := migrations.GetPostgresMigrations()

status, err := runner.GetMigrationStatus(ctx, allMigrations)
if err != nil {
return fmt.Errorf("failed to get migration status: %w", err)
}

fmt.Println("PostgreSQL Migration Status:")
fmt.Println()
for _, s := range status {
statusIcon := icons.StyledCrossMark()
statusText := "Pending"
if s.Applied {
statusIcon = icons.StyledCheckMark()
statusText = "Applied"
}
fmt.Printf(" %s Version %s: %s (%s)\n", statusIcon, s.Version, s.Description, statusText)
}

return nil
}
Loading