Add a new `picoclaw migrate` CLI command that detects an existing OpenClaw installation and migrates workspace files and configuration to PicoClaw. Workspace markdown files (SOUL.md, AGENTS.md, USER.md, TOOLS.md, HEARTBEAT.md, memory/, skills/) are copied 1:1. Config keys are mapped from OpenClaw's camelCase JSON format to PicoClaw's snake_case format with provider and channel field mapping. Supports --dry-run, --refresh, --config-only, --workspace-only, --force flags. Existing PicoClaw files are never silently overwritten; backups are created. Closes #27 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
107 lines
2.1 KiB
Go
107 lines
2.1 KiB
Go
package migrate
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
var migrateableFiles = []string{
|
|
"AGENTS.md",
|
|
"SOUL.md",
|
|
"USER.md",
|
|
"TOOLS.md",
|
|
"HEARTBEAT.md",
|
|
}
|
|
|
|
var migrateableDirs = []string{
|
|
"memory",
|
|
"skills",
|
|
}
|
|
|
|
func PlanWorkspaceMigration(srcWorkspace, dstWorkspace string, force bool) ([]Action, error) {
|
|
var actions []Action
|
|
|
|
for _, filename := range migrateableFiles {
|
|
src := filepath.Join(srcWorkspace, filename)
|
|
dst := filepath.Join(dstWorkspace, filename)
|
|
action := planFileCopy(src, dst, force)
|
|
if action.Type != ActionSkip || action.Description != "" {
|
|
actions = append(actions, action)
|
|
}
|
|
}
|
|
|
|
for _, dirname := range migrateableDirs {
|
|
srcDir := filepath.Join(srcWorkspace, dirname)
|
|
if _, err := os.Stat(srcDir); os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
dirActions, err := planDirCopy(srcDir, filepath.Join(dstWorkspace, dirname), force)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
actions = append(actions, dirActions...)
|
|
}
|
|
|
|
return actions, nil
|
|
}
|
|
|
|
func planFileCopy(src, dst string, force bool) Action {
|
|
if _, err := os.Stat(src); os.IsNotExist(err) {
|
|
return Action{
|
|
Type: ActionSkip,
|
|
Source: src,
|
|
Destination: dst,
|
|
Description: "source file not found",
|
|
}
|
|
}
|
|
|
|
_, dstExists := os.Stat(dst)
|
|
if dstExists == nil && !force {
|
|
return Action{
|
|
Type: ActionBackup,
|
|
Source: src,
|
|
Destination: dst,
|
|
Description: "destination exists, will backup and overwrite",
|
|
}
|
|
}
|
|
|
|
return Action{
|
|
Type: ActionCopy,
|
|
Source: src,
|
|
Destination: dst,
|
|
Description: "copy file",
|
|
}
|
|
}
|
|
|
|
func planDirCopy(srcDir, dstDir string, force bool) ([]Action, error) {
|
|
var actions []Action
|
|
|
|
err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
relPath, err := filepath.Rel(srcDir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dst := filepath.Join(dstDir, relPath)
|
|
|
|
if info.IsDir() {
|
|
actions = append(actions, Action{
|
|
Type: ActionCreateDir,
|
|
Destination: dst,
|
|
Description: "create directory",
|
|
})
|
|
return nil
|
|
}
|
|
|
|
action := planFileCopy(path, dst, force)
|
|
actions = append(actions, action)
|
|
return nil
|
|
})
|
|
|
|
return actions, err
|
|
}
|