From c596e058cd215ba96639b104d7163e248013d82e Mon Sep 17 00:00:00 2001 From: Aditya Borakati <41518783+ABorakati@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:52:26 +0100 Subject: [PATCH] fix(dev): make worktree setup run on Windows (#2431) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `worktree.setup` ran two POSIX-only command strings, but lifecycle commands go through PowerShell on Windows, so worktree creation failed at: PASEO_DEV_MANAGED_HOME=1 PASEO_DEV_SEED_HOME=... ./scripts/dev-home.sh -> PASEO_DEV_MANAGED_HOME=1 : The term ... is not recognized PowerShell has no `VAR=value cmd` prefix syntax. The `cp` entry was broken the same way: `$PASEO_SOURCE_CHECKOUT_PATH` is an undefined *PowerShell* variable, not an env var, so it expanded to empty and the copy resolved to `/packages/server/.env`. Neither entry can be expressed portably in a single shell string, and `bash` is not guaranteed on Windows, so move both steps into a Node script that reads its inputs from `process.env` — matching the existing `node ./scripts/seed-ios-native-cache.mjs` entry. One code path, no platform branching. `scripts/dev-home.sh` is unchanged and still sourced by the bash service scripts; only the setup-time seeding is ported. One behavior change: a missing `packages/server/.env` in the source checkout is now skipped with a log instead of aborting setup. It is untracked local config, and the old `cp` hard-failed worktree creation for anyone without one. Co-authored-by: ABorakati Co-authored-by: Claude Opus 5 --- docs/development.md | 8 +++ paseo.json | 5 +- scripts/seed-worktree-dev-state.mjs | 95 +++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 scripts/seed-worktree-dev-state.mjs diff --git a/docs/development.md b/docs/development.md index 9eed78a00..cd96239ba 100644 --- a/docs/development.md +++ b/docs/development.md @@ -242,6 +242,14 @@ commands use the same non-login Bash behavior on macOS/Linux, but preserve their existing `cmd.exe /c` string semantics on Windows. Service scripts are separate: they launch in a terminal and receive the service environment described below. +Because the shell differs per platform, a lifecycle command that must run +everywhere cannot use POSIX-only syntax — `VAR=1 cmd` env prefixes, `$VAR` +expansion, `cp`/`rm`, or a `./scripts/*.sh` entrypoint all fail under PowerShell, +and `bash` is not guaranteed to exist on Windows. Put that logic in a Node script +that reads what it needs from `process.env` and invoke it as +`node ./scripts/.mjs`. This repo's own setup does exactly that in +`scripts/seed-worktree-dev-state.mjs` and `scripts/seed-ios-native-cache.mjs`. + ```json { "worktree": { diff --git a/paseo.json b/paseo.json index ab984058e..076d1f2c5 100644 --- a/paseo.json +++ b/paseo.json @@ -3,10 +3,9 @@ "setup": [ "npm ci", "node ./scripts/seed-ios-native-cache.mjs", - "PASEO_DEV_MANAGED_HOME=1 PASEO_DEV_SEED_HOME=\"$PASEO_SOURCE_CHECKOUT_PATH/.dev/paseo-home\" PASEO_HOME=\"$PWD/.dev/paseo-home\" ./scripts/dev-home.sh", + "node ./scripts/seed-worktree-dev-state.mjs", "npm run build:server", - "npm run build --workspace=@getpaseo/expo-two-way-audio", - "cp \"$PASEO_SOURCE_CHECKOUT_PATH/packages/server/.env\" \"$PWD/packages/server/.env\"" + "npm run build --workspace=@getpaseo/expo-two-way-audio" ] }, "scripts": { diff --git a/scripts/seed-worktree-dev-state.mjs b/scripts/seed-worktree-dev-state.mjs new file mode 100644 index 000000000..d64578959 --- /dev/null +++ b/scripts/seed-worktree-dev-state.mjs @@ -0,0 +1,95 @@ +#!/usr/bin/env node +import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs"; +import { dirname, join } from "node:path"; + +// Worktree setup runs through a stable script shell: bash on macOS/Linux, but PowerShell +// on Windows. POSIX-only command strings (`VAR=1 cmd` env prefixes, `$VAR` expansion, `cp`, +// `./scripts/*.sh`) cannot express this step portably, so the seeding lives in Node — the +// one interpreter every Paseo checkout already depends on. + +const sourceRoot = process.env.PASEO_SOURCE_CHECKOUT_PATH; +const targetRoot = process.env.PASEO_WORKTREE_PATH || process.cwd(); + +if (!sourceRoot || sourceRoot === targetRoot) { + process.exit(0); +} + +seedPaseoHome(); +copyServerEnv(); + +function seedPaseoHome() { + const source = process.env.PASEO_DEV_SEED_HOME || join(sourceRoot, ".dev/paseo-home"); + const target = join(targetRoot, ".dev/paseo-home"); + + if (!existsSync(source)) { + console.log(` Seed: skipped (${source} missing)`); + return; + } + + if (source === target) { + console.log(" Seed: skipped (source is target)"); + return; + } + + if (process.env.PASEO_DEV_RESET_HOME === "1") { + rmSync(target, { recursive: true, force: true }); + } else if (hasEntries(target)) { + console.log(` Seed: skipped (${target} already has data)`); + return; + } + + mkdirSync(target, { recursive: true }); + console.log(` Seed: copying metadata from ${source}`); + copyJsonTree(join(source, "agents"), join(target, "agents")); + copyJsonTree(join(source, "projects"), join(target, "projects")); + + const config = join(source, "config.json"); + if (existsSync(config)) { + cpSync(config, join(target, "config.json")); + } + + console.log(` Seed: copied metadata from ${source}`); +} + +// Durable JSON metadata only. Runtime files (pid files, sockets, logs) must not be copied. +function copyJsonTree(source, target) { + if (!existsSync(source)) { + return; + } + + cpSync(source, target, { + recursive: true, + filter: (path) => isDirectory(path) || path.endsWith(".json"), + }); +} + +function copyServerEnv() { + const source = join(sourceRoot, "packages/server/.env"); + const target = join(targetRoot, "packages/server/.env"); + + // Untracked local config. A checkout without one is normal, so this is not a setup failure. + if (!existsSync(source)) { + console.log(` Env: skipped (${source} missing)`); + return; + } + + mkdirSync(dirname(target), { recursive: true }); + cpSync(source, target); + console.log(` Env: copied ${source}`); +} + +function hasEntries(path) { + try { + return readdirSync(path).length > 0; + } catch { + return false; + } +} + +function isDirectory(path) { + try { + return statSync(path).isDirectory(); + } catch { + return false; + } +}