mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
8207c1c7e6
* * update migrate * * rename handlers to sources * * delete dead code * * fix go test error
321 lines
8.4 KiB
Go
321 lines
8.4 KiB
Go
package migrate
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/migrate/internal"
|
|
"github.com/sipeed/picoclaw/pkg/migrate/sources/openclaw"
|
|
)
|
|
|
|
type (
|
|
Options = internal.Options
|
|
Operation = internal.Operation
|
|
ActionType = internal.ActionType
|
|
Action = internal.Action
|
|
Result = internal.Result
|
|
HandlerFactory = internal.HandlerFactory
|
|
)
|
|
|
|
const (
|
|
ActionCopy = internal.ActionCopy
|
|
ActionSkip = internal.ActionSkip
|
|
ActionBackup = internal.ActionBackup
|
|
ActionConvertConfig = internal.ActionConvertConfig
|
|
ActionCreateDir = internal.ActionCreateDir
|
|
ActionMergeConfig = internal.ActionMergeConfig
|
|
)
|
|
|
|
type MigrateInstance struct {
|
|
options Options
|
|
handlers map[string]Operation
|
|
}
|
|
|
|
func NewMigrateInstance(opts Options) *MigrateInstance {
|
|
instance := &MigrateInstance{
|
|
options: opts,
|
|
handlers: make(map[string]Operation),
|
|
}
|
|
|
|
openclaw_handler, err := openclaw.NewOpenclawHandler(opts)
|
|
if err == nil {
|
|
instance.Register(openclaw_handler.GetSourceName(), openclaw_handler)
|
|
}
|
|
|
|
return instance
|
|
}
|
|
|
|
func (m *MigrateInstance) Register(moduleName string, module Operation) {
|
|
m.handlers[moduleName] = module
|
|
}
|
|
|
|
func (m *MigrateInstance) getCurrentHandler() (Operation, error) {
|
|
source := m.options.Source
|
|
if source == "" {
|
|
source = "openclaw"
|
|
}
|
|
handler, ok := m.handlers[source]
|
|
if !ok {
|
|
return nil, fmt.Errorf("Source '%s' not found", source)
|
|
}
|
|
return handler, nil
|
|
}
|
|
|
|
func (m *MigrateInstance) Run(opts Options) (*Result, error) {
|
|
handler, err := m.getCurrentHandler()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if opts.ConfigOnly && opts.WorkspaceOnly {
|
|
return nil, fmt.Errorf("--config-only and --workspace-only are mutually exclusive")
|
|
}
|
|
|
|
if opts.Refresh {
|
|
opts.WorkspaceOnly = true
|
|
}
|
|
|
|
sourceHome, err := handler.GetSourceHome()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
targetHome, err := internal.ResolveTargetHome(opts.TargetHome)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err = os.Stat(sourceHome); os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("Source installation not found at %s", sourceHome)
|
|
}
|
|
|
|
actions, warnings, err := m.Plan(opts, sourceHome, targetHome)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fmt.Println("Migrating from Source to PicoClaw")
|
|
fmt.Printf(" Source: %s\n", sourceHome)
|
|
fmt.Printf(" Target: %s\n", targetHome)
|
|
fmt.Println()
|
|
|
|
if opts.DryRun {
|
|
PrintPlan(actions, warnings)
|
|
return &Result{Warnings: warnings}, nil
|
|
}
|
|
|
|
if !opts.Force {
|
|
PrintPlan(actions, warnings)
|
|
if !Confirm() {
|
|
fmt.Println("Aborted.")
|
|
return &Result{Warnings: warnings}, nil
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
result := m.Execute(actions, sourceHome, targetHome)
|
|
result.Warnings = warnings
|
|
return result, nil
|
|
}
|
|
|
|
func (m *MigrateInstance) Plan(opts Options, sourceHome, targetHome string) ([]Action, []string, error) {
|
|
var actions []Action
|
|
var warnings []string
|
|
handler, err := m.getCurrentHandler()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
force := opts.Force || opts.Refresh
|
|
|
|
if !opts.WorkspaceOnly {
|
|
configPath, err := handler.GetSourceConfigFile()
|
|
if err != nil {
|
|
if opts.ConfigOnly {
|
|
return nil, nil, err
|
|
}
|
|
warnings = append(warnings, fmt.Sprintf("Config migration skipped: %v", err))
|
|
} else {
|
|
actions = append(actions, Action{
|
|
Type: ActionConvertConfig,
|
|
Source: configPath,
|
|
Target: filepath.Join(targetHome, "config.json"),
|
|
Description: "convert Source config to PicoClaw format",
|
|
})
|
|
}
|
|
}
|
|
|
|
if !opts.ConfigOnly {
|
|
srcWorkspace, err := handler.GetSourceWorkspace()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("getting source workspace: %w", err)
|
|
}
|
|
dstWorkspace := internal.ResolveWorkspace(targetHome)
|
|
|
|
if _, err := os.Stat(srcWorkspace); err == nil {
|
|
wsActions, err := internal.PlanWorkspaceMigration(srcWorkspace, dstWorkspace,
|
|
handler.GetMigrateableFiles(),
|
|
handler.GetMigrateableDirs(),
|
|
force)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("planning workspace migration: %w", err)
|
|
}
|
|
actions = append(actions, wsActions...)
|
|
} else {
|
|
warnings = append(warnings, "Source workspace directory not found, skipping workspace migration")
|
|
}
|
|
}
|
|
|
|
return actions, warnings, nil
|
|
}
|
|
|
|
func (m *MigrateInstance) Execute(actions []Action, sourceHome, targetHome string) *Result {
|
|
result := &Result{}
|
|
handler, err := m.getCurrentHandler()
|
|
if err != nil {
|
|
return result
|
|
}
|
|
|
|
for _, action := range actions {
|
|
switch action.Type {
|
|
case ActionConvertConfig:
|
|
if err := handler.ExecuteConfigMigration(action.Source, action.Target); err != nil {
|
|
result.Errors = append(result.Errors, fmt.Errorf("config migration: %w", err))
|
|
fmt.Printf(" ✗ Config migration failed: %v\n", err)
|
|
} else {
|
|
result.ConfigMigrated = true
|
|
fmt.Printf(" ✓ Converted config: %s\n", action.Target)
|
|
}
|
|
case ActionCreateDir:
|
|
if err := os.MkdirAll(action.Target, 0o755); err != nil {
|
|
result.Errors = append(result.Errors, err)
|
|
} else {
|
|
result.DirsCreated++
|
|
}
|
|
case ActionBackup:
|
|
bakPath := action.Target + ".bak"
|
|
if err := internal.CopyFile(action.Target, bakPath); err != nil {
|
|
result.Errors = append(result.Errors, fmt.Errorf("backup %s: %w", action.Target, err))
|
|
fmt.Printf(" ✗ Backup failed: %s\n", action.Target)
|
|
continue
|
|
}
|
|
result.BackupsCreated++
|
|
fmt.Printf(
|
|
" ✓ Backed up %s -> %s.bak\n",
|
|
filepath.Base(action.Target),
|
|
filepath.Base(action.Target),
|
|
)
|
|
|
|
if err := os.MkdirAll(filepath.Dir(action.Target), 0o755); err != nil {
|
|
result.Errors = append(result.Errors, err)
|
|
continue
|
|
}
|
|
if err := internal.CopyFile(action.Source, action.Target); err != nil {
|
|
result.Errors = append(result.Errors, fmt.Errorf("copy %s: %w", action.Source, err))
|
|
fmt.Printf(" ✗ Copy failed: %s\n", action.Source)
|
|
} else {
|
|
result.FilesCopied++
|
|
fmt.Printf(" ✓ Copied %s\n", internal.RelPath(action.Source, sourceHome))
|
|
}
|
|
case ActionCopy:
|
|
if err := os.MkdirAll(filepath.Dir(action.Target), 0o755); err != nil {
|
|
result.Errors = append(result.Errors, err)
|
|
continue
|
|
}
|
|
if err := internal.CopyFile(action.Source, action.Target); err != nil {
|
|
result.Errors = append(result.Errors, fmt.Errorf("copy %s: %w", action.Source, err))
|
|
fmt.Printf(" ✗ Copy failed: %s\n", action.Source)
|
|
} else {
|
|
result.FilesCopied++
|
|
fmt.Printf(" ✓ Copied %s\n", internal.RelPath(action.Source, sourceHome))
|
|
}
|
|
case ActionSkip:
|
|
result.FilesSkipped++
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func Confirm() bool {
|
|
fmt.Print("Proceed with migration? (y/n): ")
|
|
var response string
|
|
fmt.Scanln(&response)
|
|
return strings.ToLower(strings.TrimSpace(response)) == "y"
|
|
}
|
|
|
|
func (m *MigrateInstance) PrintSummary(result *Result) {
|
|
fmt.Println()
|
|
parts := []string{}
|
|
if result.FilesCopied > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d files copied", result.FilesCopied))
|
|
}
|
|
if result.ConfigMigrated {
|
|
parts = append(parts, "1 config converted")
|
|
}
|
|
if result.BackupsCreated > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d backups created", result.BackupsCreated))
|
|
}
|
|
if result.FilesSkipped > 0 {
|
|
parts = append(parts, fmt.Sprintf("%d files skipped", result.FilesSkipped))
|
|
}
|
|
|
|
if len(parts) > 0 {
|
|
fmt.Printf("Migration complete! %s.\n", strings.Join(parts, ", "))
|
|
} else {
|
|
fmt.Println("Migration complete! No actions taken.")
|
|
}
|
|
|
|
if len(result.Errors) > 0 {
|
|
fmt.Println()
|
|
fmt.Printf("%d errors occurred:\n", len(result.Errors))
|
|
for _, e := range result.Errors {
|
|
fmt.Printf(" - %v\n", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
func PrintPlan(actions []Action, warnings []string) {
|
|
fmt.Println("Planned actions:")
|
|
copies := 0
|
|
skips := 0
|
|
backups := 0
|
|
configCount := 0
|
|
|
|
for _, action := range actions {
|
|
switch action.Type {
|
|
case ActionConvertConfig:
|
|
fmt.Printf(" [config] %s -> %s\n", action.Source, action.Target)
|
|
configCount++
|
|
case ActionCopy:
|
|
fmt.Printf(" [copy] %s\n", filepath.Base(action.Source))
|
|
copies++
|
|
case ActionBackup:
|
|
fmt.Printf(" [backup] %s (exists, will backup and overwrite)\n", filepath.Base(action.Target))
|
|
backups++
|
|
copies++
|
|
case ActionSkip:
|
|
if action.Description != "" {
|
|
fmt.Printf(" [skip] %s (%s)\n", filepath.Base(action.Source), action.Description)
|
|
}
|
|
skips++
|
|
case ActionCreateDir:
|
|
fmt.Printf(" [mkdir] %s\n", action.Target)
|
|
}
|
|
}
|
|
|
|
if len(warnings) > 0 {
|
|
fmt.Println()
|
|
fmt.Println("Warnings:")
|
|
for _, w := range warnings {
|
|
fmt.Printf(" - %s\n", w)
|
|
}
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Printf("%d files to copy, %d configs to convert, %d backups needed, %d skipped\n",
|
|
copies, configCount, backups, skips)
|
|
}
|