Files
picoclaw/scripts/copydir.go
T
SiYue b4a5965602 refactor(onboard,api): harden copydir repo-root detection and use platform-neutral proc attrs naming
Address latest review comments from sky5454 in PR #2654.

scripts/copydir.go:

- Improve repository root detection in a safer, more deterministic way.

- Prefer locating repo root from the script source path via runtime.Caller(), then fallback to upward search from current working directory.

- Replace .git-only root detection with repository anchor validation: go.sum, LICENSE, and .github must exist.

- Keep \ placeholder expansion and existing in-repo path guards.

- Preserve destination safety check to prevent deleting/copying to repo root.

web/backend/api:

- Rename applyLauncherWindowsProcAttrs() to applyLauncherProcAttrs() to expose a platform-independent interface name.

- Keep platform-specific behavior split by build tags: windows keeps HideWindow SysProcAttr setup, non-windows remains no-op.

- Update gateway startup path to call the renamed helper.

Why:

- Follow reviewer feedback to avoid relying on .git detection alone and prefer runtime/file-anchor based repository location.

- Improve naming clarity by making cross-platform interfaces generic while preserving OS-specific implementation details internally.

Validation:

- go test ./cmd/picoclaw/internal/onboard

- go test ./web/backend/api
2026-04-25 00:31:36 +08:00

187 lines
3.8 KiB
Go

package main
import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
)
func main() {
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "usage: go run scripts/copydir.go <src> <dst>\n")
os.Exit(2)
}
repoRoot, err := findRepoRoot()
if err != nil {
fmt.Fprintf(os.Stderr, "locate repo root: %v\n", err)
os.Exit(1)
}
src, err := normalizePathArg(os.Args[1], repoRoot)
if err != nil {
fmt.Fprintf(os.Stderr, "resolve src path: %v\n", err)
os.Exit(1)
}
dst, err := normalizePathArg(os.Args[2], repoRoot)
if err != nil {
fmt.Fprintf(os.Stderr, "resolve dst path: %v\n", err)
os.Exit(1)
}
if err := ensurePathWithinRepo(repoRoot, src); err != nil {
fmt.Fprintf(os.Stderr, "invalid src path: %v\n", err)
os.Exit(1)
}
if err := ensurePathWithinRepo(repoRoot, dst); err != nil {
fmt.Fprintf(os.Stderr, "invalid dst path: %v\n", err)
os.Exit(1)
}
if samePath(repoRoot, dst) {
fmt.Fprintln(os.Stderr, "invalid dst path: destination cannot be repo root")
os.Exit(1)
}
if err := os.RemoveAll(dst); err != nil {
fmt.Fprintf(os.Stderr, "remove %s: %v\n", dst, err)
os.Exit(1)
}
if err := copyTree(src, dst); err != nil {
fmt.Fprintf(os.Stderr, "copy %s -> %s: %v\n", src, dst, err)
os.Exit(1)
}
}
func findRepoRoot() (string, error) {
_, file, _, ok := runtime.Caller(0)
if !ok {
return "", fmt.Errorf("unable to locate copydir.go source path")
}
scriptDir := filepath.Dir(file)
candidate := filepath.Clean(filepath.Join(scriptDir, ".."))
if err := validateRepoRoot(candidate); err == nil {
return candidate, nil
}
wd, err := os.Getwd()
if err != nil {
return "", err
}
cur, err := filepath.Abs(wd)
if err != nil {
return "", err
}
for {
if err := validateRepoRoot(cur); err == nil {
return filepath.Clean(cur), nil
}
parent := filepath.Dir(cur)
if parent == cur {
return "", fmt.Errorf("could not find repository root from %s", wd)
}
cur = parent
}
}
func validateRepoRoot(root string) error {
anchors := []string{
filepath.Join(root, "go.sum"),
filepath.Join(root, "LICENSE"),
filepath.Join(root, ".github"),
}
for _, anchor := range anchors {
if _, err := os.Stat(anchor); err != nil {
return fmt.Errorf("missing repo anchor %s: %w", anchor, err)
}
}
return nil
}
func normalizePathArg(arg, repoRoot string) (string, error) {
resolved := strings.ReplaceAll(arg, "${codespace}", repoRoot)
abs, err := filepath.Abs(resolved)
if err != nil {
return "", err
}
return filepath.Clean(abs), nil
}
func ensurePathWithinRepo(repoRoot, path string) error {
rel, err := filepath.Rel(repoRoot, path)
if err != nil {
return err
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return fmt.Errorf("path %s is outside repository root %s", path, repoRoot)
}
return nil
}
func samePath(a, b string) bool {
return filepath.Clean(a) == filepath.Clean(b)
}
func copyTree(src, dst string) error {
info, err := os.Stat(src)
if err != nil {
return err
}
if !info.IsDir() {
return fmt.Errorf("source is not a directory: %s", src)
}
return filepath.Walk(src, func(path string, entry os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
target := dst
if rel != "." {
target = filepath.Join(dst, rel)
}
if entry.IsDir() {
return os.MkdirAll(target, entry.Mode())
}
return copyFile(path, target, entry.Mode())
})
}
func copyFile(src, dst string, mode os.FileMode) error {
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Close()
}