From f6bceb29a324461074136d25d40f2bf10e1c17c3 Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Sun, 12 Apr 2026 21:27:40 +0800 Subject: [PATCH 1/6] Fix Windows build flow --- web/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/Makefile b/web/Makefile index 254c439e9..0c9344a89 100644 --- a/web/Makefile +++ b/web/Makefile @@ -164,7 +164,7 @@ endif build-frontend: ifeq ($(OS),Windows_NT) - @$(POWERSHELL) "if ((-not (Test-Path -LiteralPath '$(FRONTEND_DIR)/node_modules')) -or (-not (Test-Path -LiteralPath '$(FRONTEND_DIR)/node_modules/.bin/tsc')) -or (-not (Test-Path -LiteralPath '$(FRONTEND_INSTALL_STAMP)')) -or ((Get-Content -LiteralPath '$(FRONTEND_INSTALL_STAMP)' -Raw).Trim() -ne (((Get-FileHash -LiteralPath '$(FRONTEND_DIR)/package.json' -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath '$(FRONTEND_DIR)/pnpm-lock.yaml' -Algorithm SHA256).Hash)))) { Write-Host 'Installing frontend dependencies...'; Push-Location '$(FRONTEND_DIR)'; try { pnpm install --frozen-lockfile } finally { Pop-Location }; Set-Content -LiteralPath '$(FRONTEND_INSTALL_STAMP)' -Value (((Get-FileHash -LiteralPath '$(FRONTEND_DIR)/package.json' -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath '$(FRONTEND_DIR)/pnpm-lock.yaml' -Algorithm SHA256).Hash)) -NoNewline }" + @$(POWERSHELL) "$$frontend='$(FRONTEND_DIR)'; $$stamp='$(FRONTEND_INSTALL_STAMP)'; $$pkg='$(FRONTEND_DIR)/package.json'; $$lock='$(FRONTEND_DIR)/pnpm-lock.yaml'; $$expected=((Get-FileHash -LiteralPath $$pkg -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath $$lock -Algorithm SHA256).Hash); $$nodeModules=$$frontend + '/node_modules'; $$tsc=$$nodeModules + '/.bin/tsc'; $$current=''; if (Test-Path -LiteralPath $$stamp) { $$current=(Get-Content -LiteralPath $$stamp -Raw).Trim() }; if ((-not (Test-Path -LiteralPath $$nodeModules)) -or (-not (Test-Path -LiteralPath $$tsc)) -or ($$current -ne $$expected)) { Write-Host 'Installing frontend dependencies...'; Push-Location $$frontend; try { $$env:CI='true'; pnpm install --frozen-lockfile } finally { Pop-Location; Remove-Item Env:CI -ErrorAction SilentlyContinue }; Set-Content -LiteralPath $$stamp -Value $$expected -NoNewline }" else @expected_stamp="$$(cat $(FRONTEND_DIR)/package.json $(FRONTEND_DIR)/pnpm-lock.yaml | cksum | awk '{print $$1 ":" $$2}')"; \ if [ ! -d $(FRONTEND_DIR)/node_modules ] || \ From 193e1a3cd0444a3da1fa9ee222e6c0e113dc5f5d Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Sun, 12 Apr 2026 21:27:40 +0800 Subject: [PATCH 2/6] Fix Windows build flow --- Makefile | 17 +++++++++++++++++ web/Makefile | 16 ++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/Makefile b/Makefile index acb258370..11adcef06 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,11 @@ EXT= ifeq ($(OS),Windows_NT) POWERSHELL=powershell -NoProfile -Command +<<<<<<< HEAD +======= + GO=go + WEB_GO=go +>>>>>>> 83c79087 (Fix Windows build flow) WINDOWS_GOARCH_RAW:=$(strip $(shell go env GOARCH 2>NUL)) endif @@ -40,7 +45,10 @@ GOFLAGS?=-v -tags $(GO_BUILD_TAGS) GOCACHE?=$(CURDIR)/.cache/go-build GOMODCACHE?=$(CURDIR)/.cache/go-mod GOTOOLCHAIN?=local +<<<<<<< HEAD export CGO_ENABLED +======= +>>>>>>> 83c79087 (Fix Windows build flow) export GOCACHE export GOMODCACHE export GOTOOLCHAIN @@ -207,7 +215,16 @@ build-launcher: @echo "Building picoclaw-launcher for $(PLATFORM)/$(ARCH)..." ifeq ($(OS),Windows_NT) @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" +<<<<<<< HEAD @$(MAKE) -C web build PLATFORM="$(PLATFORM)" ARCH="$(ARCH)" EXT="$(EXT)" OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" GO_BUILD_TAGS="$(GO_BUILD_TAGS)" +======= + @$(MAKE) -C web build \ + PLATFORM="$(PLATFORM)" \ + ARCH="$(ARCH)" \ + EXT="$(EXT)" \ + OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" \ + GO_BUILD_TAGS="$(GO_BUILD_TAGS)" +>>>>>>> 83c79087 (Fix Windows build flow) @$(POWERSHELL) "Copy-Item -LiteralPath '$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)' -Destination '$(BUILD_DIR)/picoclaw-launcher$(EXT)' -Force" else @mkdir -p $(BUILD_DIR) diff --git a/web/Makefile b/web/Makefile index 0c9344a89..5580827bb 100644 --- a/web/Makefile +++ b/web/Makefile @@ -10,7 +10,10 @@ GOFLAGS?=-v -tags $(GO_BUILD_TAGS) GOCACHE?=$(abspath ../.cache/go-build) GOMODCACHE?=$(abspath ../.cache/go-mod) GOTOOLCHAIN?=local +<<<<<<< HEAD export CGO_ENABLED +======= +>>>>>>> 83c79087 (Fix Windows build flow) export GOCACHE export GOMODCACHE export GOTOOLCHAIN @@ -30,6 +33,11 @@ LAUNCHER_GUI_LDFLAG= ifeq ($(OS),Windows_NT) POWERSHELL=powershell -NoProfile -Command +<<<<<<< HEAD +======= + GO=go + WEB_GO=go +>>>>>>> 83c79087 (Fix Windows build flow) WINDOWS_GOARCH_RAW:=$(strip $(shell go env GOARCH 2>NUL)) endif @@ -164,6 +172,7 @@ endif build-frontend: ifeq ($(OS),Windows_NT) +<<<<<<< HEAD @$(POWERSHELL) "$$frontend='$(FRONTEND_DIR)'; $$stamp='$(FRONTEND_INSTALL_STAMP)'; $$pkg='$(FRONTEND_DIR)/package.json'; $$lock='$(FRONTEND_DIR)/pnpm-lock.yaml'; $$expected=((Get-FileHash -LiteralPath $$pkg -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath $$lock -Algorithm SHA256).Hash); $$nodeModules=$$frontend + '/node_modules'; $$tsc=$$nodeModules + '/.bin/tsc'; $$current=''; if (Test-Path -LiteralPath $$stamp) { $$current=(Get-Content -LiteralPath $$stamp -Raw).Trim() }; if ((-not (Test-Path -LiteralPath $$nodeModules)) -or (-not (Test-Path -LiteralPath $$tsc)) -or ($$current -ne $$expected)) { Write-Host 'Installing frontend dependencies...'; Push-Location $$frontend; try { $$env:CI='true'; pnpm install --frozen-lockfile } finally { Pop-Location; Remove-Item Env:CI -ErrorAction SilentlyContinue }; Set-Content -LiteralPath $$stamp -Value $$expected -NoNewline }" else @expected_stamp="$$(cat $(FRONTEND_DIR)/package.json $(FRONTEND_DIR)/pnpm-lock.yaml | cksum | awk '{print $$1 ":" $$2}')"; \ @@ -171,6 +180,13 @@ else [ ! -x $(FRONTEND_DIR)/node_modules/.bin/tsc ] || \ [ ! -f $(FRONTEND_INSTALL_STAMP) ] || \ [ "$$(cat $(FRONTEND_INSTALL_STAMP) 2>/dev/null)" != "$$expected_stamp" ]; then \ +======= + @if not exist "$(FRONTEND_DIR)\node_modules" (echo Installing frontend dependencies... && cd $(FRONTEND_DIR) && pnpm install --frozen-lockfile --store-dir .pnpm-store) else echo Frontend dependencies already installed. +else + @if [ ! -d $(FRONTEND_DIR)/node_modules ] || \ + [ $(FRONTEND_DIR)/package.json -nt $(FRONTEND_DIR)/node_modules ] || \ + [ $(FRONTEND_DIR)/pnpm-lock.yaml -nt $(FRONTEND_DIR)/node_modules ]; then \ +>>>>>>> 83c79087 (Fix Windows build flow) echo "Installing frontend dependencies..."; \ (cd $(FRONTEND_DIR) && CI=true pnpm install --frozen-lockfile) && \ printf '%s\n' "$$expected_stamp" > $(FRONTEND_INSTALL_STAMP); \ From a977a92729052214ca6a8428dc6c656eba9a50df Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Fri, 24 Apr 2026 16:40:06 +0800 Subject: [PATCH 3/6] build(web): avoid shell-expanding powershell vars in windows recipe - rewrite build-frontend Windows command without PowerShell local vars - keep install-stamp hash check logic --- Makefile | 17 ----------------- web/Makefile | 18 +----------------- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/Makefile b/Makefile index 11adcef06..acb258370 100644 --- a/Makefile +++ b/Makefile @@ -9,11 +9,6 @@ EXT= ifeq ($(OS),Windows_NT) POWERSHELL=powershell -NoProfile -Command -<<<<<<< HEAD -======= - GO=go - WEB_GO=go ->>>>>>> 83c79087 (Fix Windows build flow) WINDOWS_GOARCH_RAW:=$(strip $(shell go env GOARCH 2>NUL)) endif @@ -45,10 +40,7 @@ GOFLAGS?=-v -tags $(GO_BUILD_TAGS) GOCACHE?=$(CURDIR)/.cache/go-build GOMODCACHE?=$(CURDIR)/.cache/go-mod GOTOOLCHAIN?=local -<<<<<<< HEAD export CGO_ENABLED -======= ->>>>>>> 83c79087 (Fix Windows build flow) export GOCACHE export GOMODCACHE export GOTOOLCHAIN @@ -215,16 +207,7 @@ build-launcher: @echo "Building picoclaw-launcher for $(PLATFORM)/$(ARCH)..." ifeq ($(OS),Windows_NT) @$(POWERSHELL) "New-Item -ItemType Directory -Force -Path '$(BUILD_DIR)' | Out-Null" -<<<<<<< HEAD @$(MAKE) -C web build PLATFORM="$(PLATFORM)" ARCH="$(ARCH)" EXT="$(EXT)" OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" GO_BUILD_TAGS="$(GO_BUILD_TAGS)" -======= - @$(MAKE) -C web build \ - PLATFORM="$(PLATFORM)" \ - ARCH="$(ARCH)" \ - EXT="$(EXT)" \ - OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)" \ - GO_BUILD_TAGS="$(GO_BUILD_TAGS)" ->>>>>>> 83c79087 (Fix Windows build flow) @$(POWERSHELL) "Copy-Item -LiteralPath '$(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH)$(EXT)' -Destination '$(BUILD_DIR)/picoclaw-launcher$(EXT)' -Force" else @mkdir -p $(BUILD_DIR) diff --git a/web/Makefile b/web/Makefile index 5580827bb..254c439e9 100644 --- a/web/Makefile +++ b/web/Makefile @@ -10,10 +10,7 @@ GOFLAGS?=-v -tags $(GO_BUILD_TAGS) GOCACHE?=$(abspath ../.cache/go-build) GOMODCACHE?=$(abspath ../.cache/go-mod) GOTOOLCHAIN?=local -<<<<<<< HEAD export CGO_ENABLED -======= ->>>>>>> 83c79087 (Fix Windows build flow) export GOCACHE export GOMODCACHE export GOTOOLCHAIN @@ -33,11 +30,6 @@ LAUNCHER_GUI_LDFLAG= ifeq ($(OS),Windows_NT) POWERSHELL=powershell -NoProfile -Command -<<<<<<< HEAD -======= - GO=go - WEB_GO=go ->>>>>>> 83c79087 (Fix Windows build flow) WINDOWS_GOARCH_RAW:=$(strip $(shell go env GOARCH 2>NUL)) endif @@ -172,21 +164,13 @@ endif build-frontend: ifeq ($(OS),Windows_NT) -<<<<<<< HEAD - @$(POWERSHELL) "$$frontend='$(FRONTEND_DIR)'; $$stamp='$(FRONTEND_INSTALL_STAMP)'; $$pkg='$(FRONTEND_DIR)/package.json'; $$lock='$(FRONTEND_DIR)/pnpm-lock.yaml'; $$expected=((Get-FileHash -LiteralPath $$pkg -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath $$lock -Algorithm SHA256).Hash); $$nodeModules=$$frontend + '/node_modules'; $$tsc=$$nodeModules + '/.bin/tsc'; $$current=''; if (Test-Path -LiteralPath $$stamp) { $$current=(Get-Content -LiteralPath $$stamp -Raw).Trim() }; if ((-not (Test-Path -LiteralPath $$nodeModules)) -or (-not (Test-Path -LiteralPath $$tsc)) -or ($$current -ne $$expected)) { Write-Host 'Installing frontend dependencies...'; Push-Location $$frontend; try { $$env:CI='true'; pnpm install --frozen-lockfile } finally { Pop-Location; Remove-Item Env:CI -ErrorAction SilentlyContinue }; Set-Content -LiteralPath $$stamp -Value $$expected -NoNewline }" + @$(POWERSHELL) "if ((-not (Test-Path -LiteralPath '$(FRONTEND_DIR)/node_modules')) -or (-not (Test-Path -LiteralPath '$(FRONTEND_DIR)/node_modules/.bin/tsc')) -or (-not (Test-Path -LiteralPath '$(FRONTEND_INSTALL_STAMP)')) -or ((Get-Content -LiteralPath '$(FRONTEND_INSTALL_STAMP)' -Raw).Trim() -ne (((Get-FileHash -LiteralPath '$(FRONTEND_DIR)/package.json' -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath '$(FRONTEND_DIR)/pnpm-lock.yaml' -Algorithm SHA256).Hash)))) { Write-Host 'Installing frontend dependencies...'; Push-Location '$(FRONTEND_DIR)'; try { pnpm install --frozen-lockfile } finally { Pop-Location }; Set-Content -LiteralPath '$(FRONTEND_INSTALL_STAMP)' -Value (((Get-FileHash -LiteralPath '$(FRONTEND_DIR)/package.json' -Algorithm SHA256).Hash + ':' + (Get-FileHash -LiteralPath '$(FRONTEND_DIR)/pnpm-lock.yaml' -Algorithm SHA256).Hash)) -NoNewline }" else @expected_stamp="$$(cat $(FRONTEND_DIR)/package.json $(FRONTEND_DIR)/pnpm-lock.yaml | cksum | awk '{print $$1 ":" $$2}')"; \ if [ ! -d $(FRONTEND_DIR)/node_modules ] || \ [ ! -x $(FRONTEND_DIR)/node_modules/.bin/tsc ] || \ [ ! -f $(FRONTEND_INSTALL_STAMP) ] || \ [ "$$(cat $(FRONTEND_INSTALL_STAMP) 2>/dev/null)" != "$$expected_stamp" ]; then \ -======= - @if not exist "$(FRONTEND_DIR)\node_modules" (echo Installing frontend dependencies... && cd $(FRONTEND_DIR) && pnpm install --frozen-lockfile --store-dir .pnpm-store) else echo Frontend dependencies already installed. -else - @if [ ! -d $(FRONTEND_DIR)/node_modules ] || \ - [ $(FRONTEND_DIR)/package.json -nt $(FRONTEND_DIR)/node_modules ] || \ - [ $(FRONTEND_DIR)/pnpm-lock.yaml -nt $(FRONTEND_DIR)/node_modules ]; then \ ->>>>>>> 83c79087 (Fix Windows build flow) echo "Installing frontend dependencies..."; \ (cd $(FRONTEND_DIR) && CI=true pnpm install --frozen-lockfile) && \ printf '%s\n' "$$expected_stamp" > $(FRONTEND_INSTALL_STAMP); \ From e1863234f0370a2b6325e203ed83910dbb754112 Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Fri, 24 Apr 2026 16:50:19 +0800 Subject: [PATCH 4/6] fix(launcher): hide windows child-process console flashes - hide windows when launching gateway process from launcher - hide windows for powershell/tasklist process inspection commands --- web/backend/api/exec_nonwindows.go | 11 +++++++++++ web/backend/api/exec_windows.go | 24 ++++++++++++++++++++++++ web/backend/api/gateway.go | 9 +++++---- 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 web/backend/api/exec_nonwindows.go create mode 100644 web/backend/api/exec_windows.go diff --git a/web/backend/api/exec_nonwindows.go b/web/backend/api/exec_nonwindows.go new file mode 100644 index 000000000..a68a3bfd7 --- /dev/null +++ b/web/backend/api/exec_nonwindows.go @@ -0,0 +1,11 @@ +//go:build !windows + +package api + +import "os/exec" + +func launcherExecCommand(name string, args ...string) *exec.Cmd { + return exec.Command(name, args...) +} + +func applyLauncherWindowsProcAttrs(_ *exec.Cmd) {} diff --git a/web/backend/api/exec_windows.go b/web/backend/api/exec_windows.go new file mode 100644 index 000000000..1e76f8c73 --- /dev/null +++ b/web/backend/api/exec_windows.go @@ -0,0 +1,24 @@ +//go:build windows + +package api + +import ( + "os/exec" + "syscall" +) + +func launcherExecCommand(name string, args ...string) *exec.Cmd { + cmd := exec.Command(name, args...) + applyLauncherWindowsProcAttrs(cmd) + return cmd +} + +func applyLauncherWindowsProcAttrs(cmd *exec.Cmd) { + if cmd == nil { + return + } + if cmd.SysProcAttr == nil { + cmd.SysProcAttr = &syscall.SysProcAttr{} + } + cmd.SysProcAttr.HideWindow = true +} diff --git a/web/backend/api/gateway.go b/web/backend/api/gateway.go index 201000ff3..d3e5ae1d5 100644 --- a/web/backend/api/gateway.go +++ b/web/backend/api/gateway.go @@ -164,7 +164,7 @@ func isLikelyGatewayProcess(pid int) (bool, bool) { `$p=Get-CimInstance Win32_Process -Filter "ProcessId = %d"; if ($null -eq $p) { "" } else { $p.CommandLine }`, pid, ) - out, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", psCmd).Output() + out, err := launcherExecCommand("powershell", "-NoProfile", "-NonInteractive", "-Command", psCmd).Output() if err == nil { cmdline := strings.TrimSpace(string(out)) if cmdline != "" { @@ -173,7 +173,7 @@ func isLikelyGatewayProcess(pid int) (bool, bool) { } // Fallback: determine only whether the process still exists. - out, err = exec.Command("tasklist", "/FI", "PID eq "+strconv.Itoa(pid), "/FO", "CSV", "/NH").Output() + out, err = launcherExecCommand("tasklist", "/FI", "PID eq "+strconv.Itoa(pid), "/FO", "CSV", "/NH").Output() if err != nil { return false, false } @@ -187,7 +187,7 @@ func isLikelyGatewayProcess(pid int) (bool, bool) { if strings.Contains(line, "\"picoclaw.exe\"") { return true, true } - return false, false + return false, true } if strings.Contains(line, "no tasks are running") { return false, true @@ -195,7 +195,7 @@ func isLikelyGatewayProcess(pid int) (bool, bool) { return false, true } - out, err := exec.Command("ps", "-o", "command=", "-p", strconv.Itoa(pid)).Output() + out, err := launcherExecCommand("ps", "-o", "command=", "-p", strconv.Itoa(pid)).Output() if err != nil { return false, false } @@ -706,6 +706,7 @@ func (h *Handler) startGatewayLocked(initialStatus string, existingPid int) (int logger.InfoC("gateway", fmt.Sprintf("Starting gateway process (%s)", execPath)) cmd = gatewayExecCommand(execPath, h.gatewayCommandArgs()...) + applyLauncherWindowsProcAttrs(cmd) cmd.Env = os.Environ() // Forward the launcher's config path via the environment variable that // GetConfigPath() already reads, so the gateway sub-process uses the same From 494cc381b5bf390bcd4243627ef12dd78dd50ec6 Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Fri, 24 Apr 2026 23:13:09 +0800 Subject: [PATCH 5/6] build(onboard): support codespace placeholder and path checks --- cmd/picoclaw/internal/onboard/command.go | 2 +- scripts/copydir.go | 80 +++++++++++++++++++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/cmd/picoclaw/internal/onboard/command.go b/cmd/picoclaw/internal/onboard/command.go index bf8f4104f..3f0ff0d8d 100644 --- a/cmd/picoclaw/internal/onboard/command.go +++ b/cmd/picoclaw/internal/onboard/command.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/cobra" ) -//go:generate go run ../../../../scripts/copydir.go ../../../../workspace ./workspace +//go:generate go run ../../../../scripts/copydir.go "${DOLLAR}{codespace}/workspace" ./workspace //go:embed workspace var embeddedFiles embed.FS diff --git a/scripts/copydir.go b/scripts/copydir.go index 74eff6c72..35622ab17 100644 --- a/scripts/copydir.go +++ b/scripts/copydir.go @@ -5,6 +5,7 @@ import ( "io" "os" "path/filepath" + "strings" ) func main() { @@ -13,8 +14,36 @@ func main() { os.Exit(2) } - src := os.Args[1] - dst := os.Args[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) @@ -27,6 +56,53 @@ func main() { } } +func findRepoRoot() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + + cur, err := filepath.Abs(wd) + if err != nil { + return "", err + } + + for { + if _, err := os.Stat(filepath.Join(cur, ".git")); err == nil { + return filepath.Clean(cur), nil + } + parent := filepath.Dir(cur) + if parent == cur { + return "", fmt.Errorf("could not find .git from %s", wd) + } + cur = parent + } +} + +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 { From b4a59656025c6eb863ec4755b67a58820aab6d69 Mon Sep 17 00:00:00 2001 From: SiYue <2835601846@qq.com> Date: Sat, 25 Apr 2026 00:31:36 +0800 Subject: [PATCH 6/6] 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 --- scripts/copydir.go | 30 ++++++++++++++++++++++++++++-- web/backend/api/exec_nonwindows.go | 2 +- web/backend/api/exec_windows.go | 4 ++-- web/backend/api/gateway.go | 2 +- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/scripts/copydir.go b/scripts/copydir.go index 35622ab17..6e2777612 100644 --- a/scripts/copydir.go +++ b/scripts/copydir.go @@ -5,6 +5,7 @@ import ( "io" "os" "path/filepath" + "runtime" "strings" ) @@ -57,6 +58,17 @@ func main() { } 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 @@ -68,17 +80,31 @@ func findRepoRoot() (string, error) { } for { - if _, err := os.Stat(filepath.Join(cur, ".git")); err == nil { + if err := validateRepoRoot(cur); err == nil { return filepath.Clean(cur), nil } parent := filepath.Dir(cur) if parent == cur { - return "", fmt.Errorf("could not find .git from %s", wd) + 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) diff --git a/web/backend/api/exec_nonwindows.go b/web/backend/api/exec_nonwindows.go index a68a3bfd7..0dc3c0e94 100644 --- a/web/backend/api/exec_nonwindows.go +++ b/web/backend/api/exec_nonwindows.go @@ -8,4 +8,4 @@ func launcherExecCommand(name string, args ...string) *exec.Cmd { return exec.Command(name, args...) } -func applyLauncherWindowsProcAttrs(_ *exec.Cmd) {} +func applyLauncherProcAttrs(_ *exec.Cmd) {} diff --git a/web/backend/api/exec_windows.go b/web/backend/api/exec_windows.go index 1e76f8c73..86d3193a0 100644 --- a/web/backend/api/exec_windows.go +++ b/web/backend/api/exec_windows.go @@ -9,11 +9,11 @@ import ( func launcherExecCommand(name string, args ...string) *exec.Cmd { cmd := exec.Command(name, args...) - applyLauncherWindowsProcAttrs(cmd) + applyLauncherProcAttrs(cmd) return cmd } -func applyLauncherWindowsProcAttrs(cmd *exec.Cmd) { +func applyLauncherProcAttrs(cmd *exec.Cmd) { if cmd == nil { return } diff --git a/web/backend/api/gateway.go b/web/backend/api/gateway.go index d3e5ae1d5..606c8351d 100644 --- a/web/backend/api/gateway.go +++ b/web/backend/api/gateway.go @@ -706,7 +706,7 @@ func (h *Handler) startGatewayLocked(initialStatus string, existingPid int) (int logger.InfoC("gateway", fmt.Sprintf("Starting gateway process (%s)", execPath)) cmd = gatewayExecCommand(execPath, h.gatewayCommandArgs()...) - applyLauncherWindowsProcAttrs(cmd) + applyLauncherProcAttrs(cmd) cmd.Env = os.Environ() // Forward the launcher's config path via the environment variable that // GetConfigPath() already reads, so the gateway sub-process uses the same