mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Stabilize local dev daemon homes
Use a checkout-local .dev/paseo-home for root and worktree dev flows so development daemons do not collide with the packaged app home. Split server, app, and desktop dev entrypoints, seed worktree homes from the source checkout metadata, and keep desktop dev on its own user-data directory and Expo port.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -64,6 +64,7 @@ CLAUDE.local.md
|
|||||||
|
|
||||||
.debug.conversations/
|
.debug.conversations/
|
||||||
.debug/
|
.debug/
|
||||||
|
.dev/
|
||||||
.paseo/
|
.paseo/
|
||||||
.wrangler/
|
.wrangler/
|
||||||
**/.wrangler/
|
**/.wrangler/
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"typeAware": false
|
"typeAware": false
|
||||||
},
|
},
|
||||||
|
"ignorePatterns": [".dev/**"],
|
||||||
"plugins": ["react", "react-perf", "unicorn", "typescript", "oxc", "import", "promise"],
|
"plugins": ["react", "react-perf", "unicorn", "typescript", "oxc", "import", "promise"],
|
||||||
"categories": {
|
"categories": {
|
||||||
"correctness": "error",
|
"correctness": "error",
|
||||||
|
|||||||
@@ -8,18 +8,26 @@
|
|||||||
## Running the dev server
|
## Running the dev server
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev:server
|
||||||
|
npm run dev:app
|
||||||
|
npm run dev:desktop
|
||||||
```
|
```
|
||||||
|
|
||||||
`scripts/dev.sh` runs the daemon and Expo together via `concurrently`, fronted by [`portless`](https://www.npmjs.com/package/portless) so each service is reachable at a stable name like `https://daemon.localhost` / `https://app.localhost` instead of a fixed port. The underlying TCP ports are ephemeral — never hardcode them. (Windows uses `scripts/dev.ps1`, which still binds the daemon to `localhost:6767` directly.)
|
Root checkout dev is intentionally split across terminals:
|
||||||
|
|
||||||
|
- `npm run dev:server` runs the daemon on `127.0.0.1:6768`.
|
||||||
|
- `npm run dev:app` runs Expo on `http://localhost:8081` and connects to the dev daemon.
|
||||||
|
- `npm run dev:desktop` runs its own Electron-flavored Expo server on the first free port from `8082` through `8089`. It never claims port `8081`.
|
||||||
|
|
||||||
|
`npm run dev` is only a shorthand for `npm run dev:server`. Keep `127.0.0.1:6767` for the packaged app and production-style `~/.paseo` state.
|
||||||
|
|
||||||
### PASEO_HOME
|
### PASEO_HOME
|
||||||
|
|
||||||
`PASEO_HOME` is the directory that holds runtime state (agents, sockets, daemon log). Resolution rules:
|
`PASEO_HOME` is the directory that holds runtime state (agents, sockets, daemon log). Resolution rules:
|
||||||
|
|
||||||
- The **server itself** (e.g. when launched by the desktop app or `npm run start`) defaults to `~/.paseo` (see `packages/server/src/server/paseo-home.ts`).
|
- The **server itself** (e.g. when launched by the desktop app or `npm run start`) defaults to `~/.paseo` (see `packages/server/src/server/paseo-home.ts`).
|
||||||
- **`npm run dev` from a git worktree** derives a stable home like `~/.paseo-<worktree-name>` and, on first run, seeds it from `~/.paseo` by copying agent/project JSON metadata and `config.json`. Checkout/worktree directories are not copied.
|
- **Repo dev scripts** default to `$ROOT/.dev/paseo-home`, where `$ROOT` is the current checkout or worktree root. This keeps all dev state disposable with the checkout.
|
||||||
- **`npm run dev` from the main checkout** (not a worktree) uses a fresh `mktemp` directory under `$TMPDIR` and removes it on exit. Set `PASEO_HOME` explicitly to keep state across runs.
|
- **Paseo-created worktrees** seed `$PASEO_WORKTREE_PATH/.dev/paseo-home` from `$PASEO_SOURCE_CHECKOUT_PATH/.dev/paseo-home` by copying durable JSON metadata. Runtime files like pid files, sockets, and logs are not copied.
|
||||||
|
|
||||||
Override knobs:
|
Override knobs:
|
||||||
|
|
||||||
@@ -32,16 +40,19 @@ PASEO_DEV_RESET_HOME=1 npm run dev # clear and reseed the derived wor
|
|||||||
### Daemon endpoints
|
### Daemon endpoints
|
||||||
|
|
||||||
- Stable daemon launched by the desktop app: `localhost:6767`.
|
- Stable daemon launched by the desktop app: `localhost:6767`.
|
||||||
- `npm run dev` (macOS/Linux): portless URLs only — read them from the `dev.sh` banner or `portless get daemon` / `portless get app`.
|
- Root checkout dev daemon: `localhost:6768`.
|
||||||
|
- Root checkout Expo: `http://localhost:8081`.
|
||||||
|
- Root checkout desktop dev Expo: first free port from `8082` through `8089`.
|
||||||
- `npm run dev` (Windows): `localhost:6767` for the daemon.
|
- `npm run dev` (Windows): `localhost:6767` for the daemon.
|
||||||
|
|
||||||
In any worktree-style or portless setup, never assume default ports.
|
In Paseo-managed worktree services, use the injected service environment rather than hardcoded root checkout ports.
|
||||||
|
|
||||||
### Desktop renderer profiling
|
### Desktop renderer profiling
|
||||||
|
|
||||||
`npm run dev:desktop` starts Electron with Chromium remote debugging enabled on
|
`npm run dev:desktop` starts Electron with Chromium remote debugging enabled on
|
||||||
`http://127.0.0.1:9223` so renderer CPU profiles can be captured through CDP.
|
`http://127.0.0.1:9223` so renderer CPU profiles can be captured through CDP.
|
||||||
Override the port with `PASEO_ELECTRON_REMOTE_DEBUGGING_PORT` when `9223` is busy.
|
It launches its own Electron-flavored Expo server and passes that URL to Electron.
|
||||||
|
Override the CDP port with `PASEO_ELECTRON_REMOTE_DEBUGGING_PORT` when `9223` is busy.
|
||||||
|
|
||||||
### React render profiling
|
### React render profiling
|
||||||
|
|
||||||
@@ -185,7 +196,7 @@ Service proxy hostnames use the double-dash shape: `web--feature-auth--project.l
|
|||||||
|
|
||||||
Package imports resolve through package exports to compiled `dist/` output, not sibling `src/` files. This is true in local dev and in published packages: the app, daemon, CLI, and SDK consumers should all exercise the same runtime paths.
|
Package imports resolve through package exports to compiled `dist/` output, not sibling `src/` files. This is true in local dev and in published packages: the app, daemon, CLI, and SDK consumers should all exercise the same runtime paths.
|
||||||
|
|
||||||
`npm run dev`, `npm run dev:server`, and `npm run dev:app` build the workspace packages they need once, then keep `@getpaseo/protocol` and `@getpaseo/client` fresh with TypeScript watch builds while the daemon or Expo runs. If you change protocol schemas or client code outside those watch workflows, rebuild the producer before trusting runtime behavior.
|
`npm run dev:server` builds the server-side workspace packages once, then keeps `@getpaseo/protocol` and `@getpaseo/client` fresh with TypeScript watch builds while the daemon runs. If you change protocol schemas or client code outside that watch workflow, rebuild the producer before trusting runtime behavior.
|
||||||
|
|
||||||
Use the named root build targets instead of remembering workspace dependency chains:
|
Use the named root build targets instead of remembering workspace dependency chains:
|
||||||
|
|
||||||
@@ -277,7 +288,7 @@ Get the session ID from the agent JSON (`persistence.sessionId`), then:
|
|||||||
|
|
||||||
## Testing with Playwright MCP
|
## Testing with Playwright MCP
|
||||||
|
|
||||||
Point Playwright MCP at the running Expo web target. Under `npm run dev` (macOS/Linux) that is the portless URL printed in the dev banner — typically `https://app.localhost`. If you start Expo directly with `expo start --web` (no portless), Metro defaults to `http://localhost:8081`.
|
Point Playwright MCP at the running Expo web target. For root checkout dev, `npm run dev:app` reserves `http://localhost:8081`. For Paseo-managed worktree app services, use the service URL or port shown by Paseo for that worktree.
|
||||||
|
|
||||||
Do NOT use browser history (back/forward). Always navigate by clicking UI elements or using `browser_navigate` with the full URL — the app uses client-side routing and browser history breaks state.
|
Do NOT use browser history (back/forward). Always navigate by clicking UI elements or using `browser_navigate` with the full URL — the app uses client-side routing and browser history breaks state.
|
||||||
|
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -34,12 +34,12 @@
|
|||||||
"packages/cli"
|
"packages/cli"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "./scripts/dev.sh",
|
"dev": "npm run dev:server",
|
||||||
"dev:win": "powershell ./scripts/dev.ps1",
|
"dev:win": "powershell ./scripts/dev.ps1",
|
||||||
"dev:server": "npm run build:server-deps && npm run dev:server:watch",
|
"dev:server": "cross-env PASEO_LISTEN=127.0.0.1:6768 ./scripts/dev-daemon.sh",
|
||||||
"dev:server:watch": "concurrently --kill-others --names protocol,client,server --prefix-colors yellow,blue,cyan \"npm run watch:protocol\" \"npm run watch:client\" \"npm run dev:server:raw\"",
|
"dev:server:watch": "concurrently --kill-others --names protocol,client,server --prefix-colors yellow,blue,cyan \"npm run watch:protocol\" \"npm run watch:client\" \"npm run dev:server:raw\"",
|
||||||
"dev:server:raw": "npm run dev --workspace=@getpaseo/server",
|
"dev:server:raw": "npm run dev --workspace=@getpaseo/server",
|
||||||
"dev:app": "npm run start --workspace=@getpaseo/app",
|
"dev:app": "cross-env PASEO_LISTEN=127.0.0.1:6768 EXPO_PORT=8081 ./scripts/dev-app.sh",
|
||||||
"dev:website": "npm run dev --workspace=@getpaseo/website",
|
"dev:website": "npm run dev --workspace=@getpaseo/website",
|
||||||
"postinstall": "node scripts/postinstall-patches.mjs",
|
"postinstall": "node scripts/postinstall-patches.mjs",
|
||||||
"prepare": "lefthook install --force",
|
"prepare": "lefthook install --force",
|
||||||
@@ -82,11 +82,11 @@
|
|||||||
"android:clean": "npm run android:clean --workspace=@getpaseo/app",
|
"android:clean": "npm run android:clean --workspace=@getpaseo/app",
|
||||||
"ios": "npm run ios --workspace=@getpaseo/app",
|
"ios": "npm run ios --workspace=@getpaseo/app",
|
||||||
"web": "npm run web --workspace=@getpaseo/app",
|
"web": "npm run web --workspace=@getpaseo/app",
|
||||||
"dev:desktop": "npm run dev --workspace=@getpaseo/desktop",
|
"dev:desktop": "cross-env PASEO_LISTEN=127.0.0.1:6768 npm run dev --workspace=@getpaseo/desktop",
|
||||||
"dev:win:desktop": "npm run dev:win --workspace=@getpaseo/desktop",
|
"dev:win:desktop": "npm run dev:win --workspace=@getpaseo/desktop",
|
||||||
"build:desktop": "npm run build:app-deps:clean && cd packages/app && cross-env PASEO_WEB_PLATFORM=electron npx expo export --platform web && cd ../.. && npm run build --workspace=@getpaseo/desktop --",
|
"build:desktop": "npm run build:app-deps:clean && cd packages/app && cross-env PASEO_WEB_PLATFORM=electron npx expo export --platform web && cd ../.. && npm run build --workspace=@getpaseo/desktop --",
|
||||||
"db:query": "npm run db:query --workspace=@getpaseo/server --",
|
"db:query": "npm run db:query --workspace=@getpaseo/server --",
|
||||||
"cli": "npx tsx packages/cli/src/index.js",
|
"cli": "./scripts/dev-home.sh npx tsx packages/cli/src/index.js",
|
||||||
"version": "npm run version:sync-internal && npm run release:prepare && git add -A",
|
"version": "npm run version:sync-internal && npm run release:prepare && git add -A",
|
||||||
"version:sync-internal": "node scripts/sync-workspace-versions.mjs",
|
"version:sync-internal": "node scripts/sync-workspace-versions.mjs",
|
||||||
"release:prepare": "npm install --workspaces --include-workspace-root",
|
"release:prepare": "npm install --workspaces --include-workspace-root",
|
||||||
|
|||||||
188
packages/desktop/scripts/dev-runner.mjs
Normal file
188
packages/desktop/scripts/dev-runner.mjs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import net from "node:net";
|
||||||
|
import path from "node:path";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const desktopDir = path.resolve(scriptDir, "..");
|
||||||
|
const rootDir = path.resolve(desktopDir, "../..");
|
||||||
|
const appDir = path.resolve(desktopDir, "../app");
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const electron = require("electron");
|
||||||
|
|
||||||
|
const expoPort = Number(process.env.EXPO_PORT);
|
||||||
|
if (!Number.isInteger(expoPort) || expoPort <= 0) {
|
||||||
|
console.error("[dev] EXPO_PORT must be set before running desktop dev");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expoDevUrl = process.env.EXPO_DEV_URL || `http://localhost:${expoPort}`;
|
||||||
|
const daemonEndpoint = process.env.PASEO_DAEMON_ENDPOINT || process.env.PASEO_LISTEN || "";
|
||||||
|
const colorEnv = {
|
||||||
|
FORCE_COLOR: process.env.FORCE_COLOR || "1",
|
||||||
|
npm_config_color: process.env.npm_config_color || "always",
|
||||||
|
};
|
||||||
|
|
||||||
|
const children = new Map();
|
||||||
|
let stopping = false;
|
||||||
|
let exitCode = 0;
|
||||||
|
|
||||||
|
function prefixStream(name, stream, target) {
|
||||||
|
let buffered = "";
|
||||||
|
stream.setEncoding("utf8");
|
||||||
|
stream.on("data", (chunk) => {
|
||||||
|
buffered += chunk;
|
||||||
|
const lines = buffered.split(/\r?\n/);
|
||||||
|
buffered = lines.pop() ?? "";
|
||||||
|
for (const line of lines) {
|
||||||
|
target.write(line ? `[${name}] ${line}\n` : `[${name}]\n`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stream.on("end", () => {
|
||||||
|
if (buffered) {
|
||||||
|
target.write(`[${name}] ${buffered}\n`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawnChild(name, command, args, options = {}) {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
cwd: rootDir,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...colorEnv,
|
||||||
|
},
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
children.set(name, child);
|
||||||
|
prefixStream(name, child.stdout, process.stdout);
|
||||||
|
prefixStream(name, child.stderr, process.stderr);
|
||||||
|
|
||||||
|
child.on("error", (error) => {
|
||||||
|
console.error(`[${name}] failed to start: ${error.message}`);
|
||||||
|
exitCode = 1;
|
||||||
|
stopAll("SIGTERM");
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("exit", (code, signal) => {
|
||||||
|
children.delete(name);
|
||||||
|
if (!stopping) {
|
||||||
|
if (code !== 0) {
|
||||||
|
exitCode = code ?? 1;
|
||||||
|
console.error(`[${name}] exited with ${signal ?? code}`);
|
||||||
|
}
|
||||||
|
stopAll("SIGTERM");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
function killChild(child, signal) {
|
||||||
|
if (!child.pid || child.killed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (child.detached) {
|
||||||
|
process.kill(-child.pid, signal);
|
||||||
|
} else {
|
||||||
|
child.kill(signal);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// The child may have exited between the liveness check and the signal.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAll(signal) {
|
||||||
|
if (stopping) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopping = true;
|
||||||
|
for (const child of children.values()) {
|
||||||
|
killChild(child, signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
const forceKill = setTimeout(() => {
|
||||||
|
for (const child of children.values()) {
|
||||||
|
killChild(child, "SIGKILL");
|
||||||
|
}
|
||||||
|
}, 2500);
|
||||||
|
forceKill.unref();
|
||||||
|
|
||||||
|
const finish = setInterval(() => {
|
||||||
|
if (children.size === 0) {
|
||||||
|
clearInterval(finish);
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForPort(port, host = "127.0.0.1", timeoutMs = 60_000) {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
if (await canConnect(port, host)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Timed out waiting for ${host}:${port}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function canConnect(port, host) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const socket = net.createConnection({ port, host });
|
||||||
|
socket.setTimeout(1000);
|
||||||
|
socket.once("connect", () => {
|
||||||
|
socket.destroy();
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
socket.once("timeout", () => {
|
||||||
|
socket.destroy();
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
socket.once("error", () => resolve(false));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("SIGINT", () => stopAll("SIGTERM"));
|
||||||
|
process.on("SIGTERM", () => stopAll("SIGTERM"));
|
||||||
|
|
||||||
|
spawnChild("metro", "npx", ["expo", "start", "--port", String(expoPort)], {
|
||||||
|
cwd: appDir,
|
||||||
|
detached: true,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...colorEnv,
|
||||||
|
BROWSER: "none",
|
||||||
|
APP_VARIANT: "development",
|
||||||
|
PASEO_WEB_PLATFORM: "electron",
|
||||||
|
EXPO_PUBLIC_LOCAL_DAEMON: daemonEndpoint,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await waitForPort(expoPort);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[dev] ${error.message}`);
|
||||||
|
exitCode = 1;
|
||||||
|
stopAll("SIGTERM");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stopping) {
|
||||||
|
spawnChild("electron", electron, [desktopDir], {
|
||||||
|
detached: true,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...colorEnv,
|
||||||
|
EXPO_DEV_URL: expoDevUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,88 +3,41 @@ set -e
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
DESKTOP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
DESKTOP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
APP_DIR="$(cd "$DESKTOP_DIR/../app" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "$DESKTOP_DIR/../.." && pwd)"
|
ROOT_DIR="$(cd "$DESKTOP_DIR/../.." && pwd)"
|
||||||
|
|
||||||
# Build the Electron main process
|
source "$ROOT_DIR/scripts/dev-home.sh"
|
||||||
npm run build:main
|
|
||||||
|
|
||||||
# Prefer Metro's stable default port so dev browser storage keeps the same
|
export PATH="$ROOT_DIR/node_modules/.bin:$PATH"
|
||||||
# localhost origin across restarts. Fall back only when earlier ports are busy.
|
export PASEO_LISTEN="${PASEO_LISTEN:-127.0.0.1:6768}"
|
||||||
EXPO_PORT=$(NO_COLOR=1 FORCE_COLOR=0 "$ROOT_DIR/node_modules/.bin/get-port" 8081 8082 8083 8084 8085)
|
configure_dev_paseo_home
|
||||||
|
|
||||||
|
DEV_ROOT="${PASEO_DEV_ROOT:-$(default_dev_paseo_root)}"
|
||||||
|
export PASEO_ELECTRON_USER_DATA_DIR="${PASEO_ELECTRON_USER_DATA_DIR:-$DEV_ROOT/.dev/user-data}"
|
||||||
|
mkdir -p "$PASEO_ELECTRON_USER_DATA_DIR"
|
||||||
|
|
||||||
|
if [ -z "${EXPO_PORT:-}" ]; then
|
||||||
|
EXPO_PORT=$(NO_COLOR=1 FORCE_COLOR=0 "$ROOT_DIR/node_modules/.bin/get-port" 8082 8083 8084 8085 8086 8087 8088 8089)
|
||||||
|
fi
|
||||||
export EXPO_PORT
|
export EXPO_PORT
|
||||||
|
export EXPO_DEV_URL="http://localhost:${EXPO_PORT}"
|
||||||
|
|
||||||
|
DAEMON_ENDPOINT="$(resolve_dev_daemon_endpoint)"
|
||||||
|
export PASEO_DAEMON_ENDPOINT="$DAEMON_ENDPOINT"
|
||||||
|
|
||||||
REMOTE_DEBUGGING_PORT="${PASEO_ELECTRON_REMOTE_DEBUGGING_PORT:-9223}"
|
REMOTE_DEBUGGING_PORT="${PASEO_ELECTRON_REMOTE_DEBUGGING_PORT:-9223}"
|
||||||
export PASEO_ELECTRON_FLAGS="${PASEO_ELECTRON_FLAGS:+$PASEO_ELECTRON_FLAGS }--remote-debugging-port=$REMOTE_DEBUGGING_PORT"
|
export PASEO_ELECTRON_FLAGS="${PASEO_ELECTRON_FLAGS:+$PASEO_ELECTRON_FLAGS }--remote-debugging-port=$REMOTE_DEBUGGING_PORT"
|
||||||
|
export PASEO_CORS_ORIGINS="${PASEO_CORS_ORIGINS:-*}"
|
||||||
|
|
||||||
# Allow any origin in dev so Electron on random localhost ports can reach
|
npm run build:main
|
||||||
# the daemon websocket. Safe here because this script is development-only
|
|
||||||
# and the daemon still binds to localhost.
|
|
||||||
export PASEO_CORS_ORIGINS="*"
|
|
||||||
|
|
||||||
# Fully isolate the dev instance from a production Paseo install so `npm run dev`
|
|
||||||
# works while the installed app is open. Without this the dev build (a) loses the
|
|
||||||
# Electron single-instance lock to the installed app and quits, and (b) ends up
|
|
||||||
# pointed at the production daemon, whose CORS allowlist rejects the Metro origin.
|
|
||||||
# PASEO_HOME defaults to a script-managed dev home. If you override it (to point
|
|
||||||
# dev at real data), we DON'T touch your config.json — only the managed home gets
|
|
||||||
# its daemon config seeded below, so we never rewrite a production ~/.paseo config.
|
|
||||||
# - PASEO_ELECTRON_USER_DATA_DIR: a separate Electron profile → separate
|
|
||||||
# single-instance lock, so dev and the installed app coexist.
|
|
||||||
# - PASEO_LISTEN: a distinct port so the dev daemon never collides with prod's 6767.
|
|
||||||
DEV_STATE_DIR="$DESKTOP_DIR/.dev"
|
|
||||||
if [ -n "${PASEO_HOME:-}" ]; then
|
|
||||||
PASEO_HOME_MANAGED=0
|
|
||||||
else
|
|
||||||
PASEO_HOME="$DEV_STATE_DIR/paseo-home"
|
|
||||||
PASEO_HOME_MANAGED=1
|
|
||||||
fi
|
|
||||||
export PASEO_HOME
|
|
||||||
export PASEO_ELECTRON_USER_DATA_DIR="${PASEO_ELECTRON_USER_DATA_DIR:-$DEV_STATE_DIR/user-data}"
|
|
||||||
mkdir -p "$PASEO_HOME" "$PASEO_ELECTRON_USER_DATA_DIR"
|
|
||||||
|
|
||||||
DEV_DAEMON_PORT="${PASEO_DEV_DAEMON_PORT:-6788}"
|
|
||||||
export PASEO_LISTEN="${PASEO_LISTEN:-127.0.0.1:$DEV_DAEMON_PORT}"
|
|
||||||
|
|
||||||
# Seed the isolated daemon config. The desktop daemon-manager decides whether a
|
|
||||||
# daemon is already running by reading `daemon.listen` from this config.json
|
|
||||||
# (it does NOT honor the PASEO_LISTEN env var) and probing that address. Without
|
|
||||||
# this it reads the default 6767, finds a production daemon there, and connects
|
|
||||||
# the dev app to prod — whose CORS allowlist then rejects the Metro origin. Pin
|
|
||||||
# the dev port + wildcard CORS in the file so the dev app starts its OWN daemon.
|
|
||||||
# ONLY seed the script-managed home: never rewrite a user-supplied PASEO_HOME
|
|
||||||
# (that could clobber a production config.json with the dev port + wildcard CORS).
|
|
||||||
if [ "$PASEO_HOME_MANAGED" = "1" ]; then
|
|
||||||
node -e '
|
|
||||||
const fs = require("fs");
|
|
||||||
const [path, port] = [process.argv[1], process.argv[2]];
|
|
||||||
let cfg = {};
|
|
||||||
try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); } catch {}
|
|
||||||
cfg.version = cfg.version || 1;
|
|
||||||
cfg.daemon = cfg.daemon || {};
|
|
||||||
cfg.daemon.listen = `127.0.0.1:${port}`;
|
|
||||||
cfg.daemon.cors = cfg.daemon.cors || {};
|
|
||||||
cfg.daemon.cors.allowedOrigins = ["*"];
|
|
||||||
fs.writeFileSync(path, JSON.stringify(cfg, null, 2));
|
|
||||||
' "$PASEO_HOME/config.json" "$DEV_DAEMON_PORT"
|
|
||||||
else
|
|
||||||
echo " (custom PASEO_HOME — leaving its config.json untouched)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "══════════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════════"
|
||||||
echo " Paseo Desktop Dev"
|
echo " Paseo Desktop Dev"
|
||||||
echo "══════════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════════"
|
||||||
echo " Metro: http://localhost:${EXPO_PORT}"
|
echo " Metro: ${EXPO_DEV_URL}"
|
||||||
echo " CDP: http://127.0.0.1:${REMOTE_DEBUGGING_PORT}"
|
echo " CDP: http://127.0.0.1:${REMOTE_DEBUGGING_PORT}"
|
||||||
echo " Daemon: ${PASEO_LISTEN} (isolated)"
|
echo " Daemon: ${PASEO_LISTEN}"
|
||||||
echo " PASEO_HOME: ${PASEO_HOME}"
|
echo " Home: ${PASEO_HOME}"
|
||||||
echo " userData: ${PASEO_ELECTRON_USER_DATA_DIR}"
|
echo " userData: ${PASEO_ELECTRON_USER_DATA_DIR}"
|
||||||
echo "══════════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
|
||||||
# Launch Metro + Electron together, kill both on exit
|
exec node "$SCRIPT_DIR/dev-runner.mjs"
|
||||||
exec "$ROOT_DIR/node_modules/.bin/concurrently" \
|
|
||||||
--kill-others \
|
|
||||||
--names "metro,electron" \
|
|
||||||
--prefix-colors "magenta,cyan" \
|
|
||||||
"cd '$APP_DIR' && PASEO_WEB_PLATFORM=electron npx expo start --port $EXPO_PORT" \
|
|
||||||
"$ROOT_DIR/node_modules/.bin/wait-on tcp:$EXPO_PORT && EXPO_DEV_URL=http://localhost:$EXPO_PORT electron '$DESKTOP_DIR'"
|
|
||||||
|
|||||||
10
paseo.json
10
paseo.json
@@ -2,6 +2,7 @@
|
|||||||
"worktree": {
|
"worktree": {
|
||||||
"setup": [
|
"setup": [
|
||||||
"npm ci",
|
"npm ci",
|
||||||
|
"cross-env PASEO_DEV_MANAGED_HOME=1 PASEO_DEV_SEED_HOME=\"$PASEO_SOURCE_CHECKOUT_PATH/.dev/paseo-home\" PASEO_HOME=\"$PASEO_WORKTREE_PATH/.dev/paseo-home\" ./scripts/dev-home.sh",
|
||||||
"npm run build:server",
|
"npm run build:server",
|
||||||
"npm run build --workspace=@getpaseo/expo-two-way-audio",
|
"npm run build --workspace=@getpaseo/expo-two-way-audio",
|
||||||
"cp \"$PASEO_SOURCE_CHECKOUT_PATH/packages/server/.env\" \"$PASEO_WORKTREE_PATH/packages/server/.env\""
|
"cp \"$PASEO_SOURCE_CHECKOUT_PATH/packages/server/.env\" \"$PASEO_WORKTREE_PATH/packages/server/.env\""
|
||||||
@@ -10,14 +11,15 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"daemon": {
|
"daemon": {
|
||||||
"type": "service",
|
"type": "service",
|
||||||
"command": "PASEO_SKIP_DEV_SERVER_BUILD=1 PASEO_LISTEN=0.0.0.0:$PASEO_PORT ./scripts/dev-daemon.sh"
|
"command": "cross-env PASEO_DEV_MANAGED_HOME=1 PASEO_DEV_ROOT=\"$PASEO_WORKTREE_PATH\" PASEO_HOME=\"$PASEO_WORKTREE_PATH/.dev/paseo-home\" PASEO_SKIP_DEV_SERVER_BUILD=1 PASEO_LISTEN=0.0.0.0:$PASEO_PORT ./scripts/dev-daemon.sh"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"type": "service",
|
"type": "service",
|
||||||
"command": "BROWSER=none EXPO_PUBLIC_LOCAL_DAEMON=\"localhost:${PASEO_SERVICE_DAEMON_PORT}\" npm run start:expo --workspace=@getpaseo/app -- --port $PASEO_PORT"
|
"command": "cross-env PASEO_DEV_MANAGED_HOME=1 PASEO_DEV_ROOT=\"$PASEO_WORKTREE_PATH\" PASEO_HOME=\"$PASEO_WORKTREE_PATH/.dev/paseo-home\" PASEO_LISTEN=0.0.0.0:${PASEO_SERVICE_DAEMON_PORT} PASEO_DEV_DAEMON_ENDPOINT=localhost:${PASEO_SERVICE_DAEMON_PORT} EXPO_PORT=$PASEO_PORT ./scripts/dev-app.sh"
|
||||||
},
|
},
|
||||||
"typecheck": {
|
"desktop": {
|
||||||
"command": "npm run typecheck"
|
"type": "service",
|
||||||
|
"command": "cross-env PASEO_DEV_MANAGED_HOME=1 PASEO_DEV_ROOT=\"$PASEO_WORKTREE_PATH\" PASEO_HOME=\"$PASEO_WORKTREE_PATH/.dev/paseo-home\" PASEO_LISTEN=0.0.0.0:${PASEO_SERVICE_DAEMON_PORT} PASEO_DEV_DAEMON_ENDPOINT=localhost:${PASEO_SERVICE_DAEMON_PORT} EXPO_PORT=$PASEO_PORT npm run dev --workspace=@getpaseo/desktop"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
scripts/dev-app.sh
Executable file
28
scripts/dev-app.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
export PATH="$ROOT_DIR/node_modules/.bin:$PATH"
|
||||||
|
|
||||||
|
source "$SCRIPT_DIR/dev-home.sh"
|
||||||
|
|
||||||
|
export PASEO_LISTEN="${PASEO_LISTEN:-127.0.0.1:6768}"
|
||||||
|
configure_dev_paseo_home
|
||||||
|
|
||||||
|
EXPO_PORT="${EXPO_PORT:-8081}"
|
||||||
|
DAEMON_ENDPOINT="$(resolve_dev_daemon_endpoint)"
|
||||||
|
|
||||||
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
echo " Paseo App Dev"
|
||||||
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
echo " Metro: http://localhost:${EXPO_PORT}"
|
||||||
|
echo " Daemon: ${DAEMON_ENDPOINT}"
|
||||||
|
echo " Home: ${PASEO_HOME}"
|
||||||
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
|
||||||
|
exec cross-env \
|
||||||
|
BROWSER="${BROWSER:-none}" \
|
||||||
|
APP_VARIANT=development \
|
||||||
|
EXPO_PUBLIC_LOCAL_DAEMON="$DAEMON_ENDPOINT" \
|
||||||
|
npm run start:expo --workspace=@getpaseo/app -- --port "$EXPO_PORT"
|
||||||
@@ -5,6 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
export PATH="$SCRIPT_DIR/../node_modules/.bin:$PATH"
|
export PATH="$SCRIPT_DIR/../node_modules/.bin:$PATH"
|
||||||
|
|
||||||
source "$SCRIPT_DIR/dev-home.sh"
|
source "$SCRIPT_DIR/dev-home.sh"
|
||||||
|
|
||||||
|
export PASEO_LISTEN="${PASEO_LISTEN:-127.0.0.1:6768}"
|
||||||
configure_dev_paseo_home
|
configure_dev_paseo_home
|
||||||
|
|
||||||
if [ -z "${PASEO_LOCAL_MODELS_DIR}" ]; then
|
if [ -z "${PASEO_LOCAL_MODELS_DIR}" ]; then
|
||||||
@@ -17,6 +19,7 @@ echo " Paseo Dev Daemon"
|
|||||||
echo "══════════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════════"
|
||||||
echo " Home: ${PASEO_HOME}"
|
echo " Home: ${PASEO_HOME}"
|
||||||
echo " Models: ${PASEO_LOCAL_MODELS_DIR}"
|
echo " Models: ${PASEO_LOCAL_MODELS_DIR}"
|
||||||
|
echo " Listen: ${PASEO_LISTEN}"
|
||||||
echo "══════════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
|
||||||
export PASEO_CORS_ORIGINS="${PASEO_CORS_ORIGINS:-*}"
|
export PASEO_CORS_ORIGINS="${PASEO_CORS_ORIGINS:-*}"
|
||||||
@@ -26,4 +29,4 @@ if [ "${PASEO_SKIP_DEV_SERVER_BUILD:-0}" = "1" ]; then
|
|||||||
exec npm run dev:server:watch
|
exec npm run dev:server:watch
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec npm run dev:server
|
exec sh -c 'npm run build:server-deps && npm run dev:server:watch'
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
default_dev_paseo_root() {
|
||||||
|
git rev-parse --show-toplevel 2>/dev/null || pwd
|
||||||
|
}
|
||||||
|
|
||||||
copy_json_tree() {
|
copy_json_tree() {
|
||||||
local source_dir="$1"
|
local source_dir="$1"
|
||||||
local target_dir="$2"
|
local target_dir="$2"
|
||||||
@@ -58,27 +62,83 @@ seed_worktree_paseo_home() {
|
|||||||
echo " Seed: copied metadata from ${source_home}"
|
echo " Seed: copied metadata from ${source_home}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configure_dev_daemon_config() {
|
||||||
|
if [ -z "${PASEO_LISTEN:-}" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$PASEO_HOME"
|
||||||
|
node -e '
|
||||||
|
const fs = require("fs");
|
||||||
|
const [path, listen] = [process.argv[1], process.argv[2]];
|
||||||
|
let cfg = {};
|
||||||
|
try { cfg = JSON.parse(fs.readFileSync(path, "utf8")); } catch {}
|
||||||
|
cfg.version = cfg.version || 1;
|
||||||
|
cfg.daemon = cfg.daemon || {};
|
||||||
|
cfg.daemon.listen = listen;
|
||||||
|
cfg.daemon.cors = cfg.daemon.cors || {};
|
||||||
|
cfg.daemon.cors.allowedOrigins = ["*"];
|
||||||
|
fs.writeFileSync(path, JSON.stringify(cfg, null, 2));
|
||||||
|
' "$PASEO_HOME/config.json" "$PASEO_LISTEN"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_dev_daemon_endpoint() {
|
||||||
|
if [ -n "${PASEO_DEV_DAEMON_ENDPOINT:-}" ]; then
|
||||||
|
echo "$PASEO_DEV_DAEMON_ENDPOINT"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${PASEO_LISTEN:-127.0.0.1:6768}" in
|
||||||
|
0.0.0.0:*) echo "localhost:${PASEO_LISTEN#0.0.0.0:}" ;;
|
||||||
|
127.0.0.1:*) echo "localhost:${PASEO_LISTEN#127.0.0.1:}" ;;
|
||||||
|
*) echo "$PASEO_LISTEN" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
configure_dev_paseo_home() {
|
configure_dev_paseo_home() {
|
||||||
if [ -n "${PASEO_HOME:-}" ]; then
|
if [ -n "${PASEO_HOME:-}" ]; then
|
||||||
export PASEO_HOME
|
export PASEO_HOME
|
||||||
|
if [ -n "${PASEO_DEV_SEED_HOME:-}" ]; then
|
||||||
|
seed_worktree_paseo_home "$PASEO_HOME"
|
||||||
|
fi
|
||||||
|
mkdir -p "$PASEO_HOME"
|
||||||
|
if [ "${PASEO_DEV_MANAGED_HOME:-0}" = "1" ] || [ -n "${PASEO_DEV_SEED_HOME:-}" ]; then
|
||||||
|
configure_dev_daemon_config
|
||||||
|
fi
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export PASEO_HOME
|
export PASEO_HOME
|
||||||
local git_dir
|
local dev_root
|
||||||
local git_common_dir
|
dev_root="${PASEO_DEV_ROOT:-$(default_dev_paseo_root)}"
|
||||||
git_dir="$(git rev-parse --git-dir 2>/dev/null || true)"
|
PASEO_HOME="$dev_root/.dev/paseo-home"
|
||||||
git_common_dir="$(git rev-parse --git-common-dir 2>/dev/null || true)"
|
export PASEO_DEV_MANAGED_HOME=1
|
||||||
if [ -n "$git_dir" ] && [ -n "$git_common_dir" ] && [ "$git_dir" != "$git_common_dir" ]; then
|
|
||||||
local worktree_root
|
if [ -n "${PASEO_DEV_SEED_HOME:-}" ]; then
|
||||||
local worktree_name
|
|
||||||
worktree_root="$(git rev-parse --show-toplevel)"
|
|
||||||
worktree_name="$(basename "$worktree_root" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g; s/--*/-/g; s/^-//; s/-$//')"
|
|
||||||
PASEO_HOME="$HOME/.paseo-${worktree_name}"
|
|
||||||
seed_worktree_paseo_home "$PASEO_HOME"
|
seed_worktree_paseo_home "$PASEO_HOME"
|
||||||
return
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PASEO_HOME="$(mktemp -d "${TMPDIR:-/tmp}/paseo-dev.XXXXXX")"
|
mkdir -p "$PASEO_HOME"
|
||||||
trap "rm -rf '$PASEO_HOME'" EXIT
|
configure_dev_daemon_config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configure_dev_command_env() {
|
||||||
|
if [ -z "${PASEO_LISTEN:-}" ]; then
|
||||||
|
if [ -n "${PASEO_SERVICE_DAEMON_PORT:-}" ]; then
|
||||||
|
export PASEO_LISTEN="0.0.0.0:${PASEO_SERVICE_DAEMON_PORT}"
|
||||||
|
else
|
||||||
|
export PASEO_LISTEN="127.0.0.1:6768"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
configure_dev_paseo_home
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||||
|
if [ "$#" -gt 0 ]; then
|
||||||
|
configure_dev_command_env
|
||||||
|
exec "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
configure_dev_paseo_home
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1,43 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Ensure node_modules/.bin is in PATH (for when script runs directly)
|
exec npm run dev:server
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
export PATH="$SCRIPT_DIR/../node_modules/.bin:$PATH"
|
|
||||||
|
|
||||||
source "$SCRIPT_DIR/dev-home.sh"
|
|
||||||
configure_dev_paseo_home
|
|
||||||
|
|
||||||
# Share speech models with the main install to avoid duplicate downloads
|
|
||||||
if [ -z "${PASEO_LOCAL_MODELS_DIR}" ]; then
|
|
||||||
export PASEO_LOCAL_MODELS_DIR="$HOME/.paseo/models/local-speech"
|
|
||||||
mkdir -p "$PASEO_LOCAL_MODELS_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "══════════════════════════════════════════════════════"
|
|
||||||
echo " Paseo Dev"
|
|
||||||
echo "══════════════════════════════════════════════════════"
|
|
||||||
echo " Home: ${PASEO_HOME}"
|
|
||||||
echo " Models: ${PASEO_LOCAL_MODELS_DIR}"
|
|
||||||
echo "══════════════════════════════════════════════════════"
|
|
||||||
|
|
||||||
npm run build:server
|
|
||||||
npm run build --workspace=@getpaseo/expo-two-way-audio
|
|
||||||
|
|
||||||
# Configure the daemon for the Portless app origin and let the app bootstrap
|
|
||||||
# through the daemon's Portless URL instead of a fixed localhost port.
|
|
||||||
APP_ORIGIN="$(portless get app)"
|
|
||||||
DAEMON_ENDPOINT="$(portless get daemon | sed -E 's#^https?://##')"
|
|
||||||
# Allow any origin in dev so Electron on random ports and Portless URLs all work.
|
|
||||||
# SECURITY: wildcard CORS is unsafe in production — only acceptable here because
|
|
||||||
# the daemon binds to localhost and this script is never used for production.
|
|
||||||
export PASEO_CORS_ORIGINS="*"
|
|
||||||
|
|
||||||
# Run both with concurrently
|
|
||||||
# BROWSER=none prevents auto-opening browser
|
|
||||||
# EXPO_PUBLIC_LOCAL_DAEMON configures the app to auto-connect to this daemon
|
|
||||||
concurrently \
|
|
||||||
--names "daemon,metro" \
|
|
||||||
--prefix-colors "cyan,magenta" \
|
|
||||||
"portless run --name daemon sh -c 'PASEO_SKIP_DEV_SERVER_BUILD=1 PASEO_LISTEN=0.0.0.0:\$PORT exec ./scripts/dev-daemon.sh'" \
|
|
||||||
"BROWSER=none APP_VARIANT=development EXPO_PUBLIC_LOCAL_DAEMON='${DAEMON_ENDPOINT}' portless run --name app npm run start:expo --workspace=@getpaseo/app"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user