Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,42 @@ then run the server with `API_BASE_URL=http://localhost:3000` +
`WEB_DIST=packages/web/dist` and open `http://localhost:3000`. (Over plain HTTP
the cookie name is `me_session`; over HTTPS it's `__Host-me_session`.)

### Developing the web UI (hot reload)

`me serve` and the hosted server both serve the **built** UI, so neither
reflects in-progress source edits. For that, run the **Vite dev server**
(`./bun run web`, port 5173) — it serves `packages/web/src` with hot-module
reload. Vite is *only* a frontend, though: it proxies `/rpc` + `/healthz` to a
backend (default `http://localhost:3000`) and injects no credentials, so
something must answer those paths. Two ways to supply that backend:

- **Local backend** — run the API server (`./bun run server`, needs local
Postgres + `.env`) on `:3000`, then `./bun run web` in another terminal. Edits
hot-reload against your local data.
- **Remote backend (e.g. production), no local server** — one command:

```bash
./bun run web:remote # defaults to https://api.memory.build
ME_SERVER=https://… ./bun run web:remote # or any other backend
```

Open `http://localhost:5173`. This is hot-reloading UI against **live**
production data — writes/deletes are real.

`web:remote` (`scripts/web-remote.ts`) spawns `me serve` on an auto-picked
free port, points the Vite dev server at it via `ME_DEV_RPC_TARGET`, and tears
both down together on Ctrl+C (or if either exits — e.g. `me serve` failing
because you're not logged in). `me serve` is what proxies `/rpc` to the remote
`…/api/v1/memory/rpc` and injects your OAuth token + active space, so the
browser stays credential-free.

**`ME_DEV_RPC_TARGET`** (read in `packages/web/vite.config.ts`) overrides the
proxy target; set it when `:3000` is occupied. Prefer an explicit `127.0.0.1`
over `localhost` to avoid IPv4/IPv6 ambiguity when an unrelated service also
holds `:3000`. Both `me serve` and the Vite dev server run the UI in **local**
mode (neither injects `window.__ME_BOOTSTRAP__`), so behavior matches production
aside from hot reload.

### Moving the hosted UI to `app.memory.build`

The design keeps this to **config + ingress — no application code changes** (the
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"docs:build": "./bun --filter @memory.build/docs-site build",
"web": "./bun --filter @memory.build/web dev",
"web:build": "./bun --filter @memory.build/web build",
"web:remote": "./bun scripts/web-remote.ts",
"generate:master-key": "./bun scripts/generate-master-key.ts",
"install:local": "./bun scripts/install-local.ts",
"lint": "biome check",
Expand Down
9 changes: 7 additions & 2 deletions packages/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ function sourcePath(relativePath: string): string {
return fileURLToPath(new URL(relativePath, import.meta.url));
}

// Backend the dev server proxies /rpc + /healthz to (a running `me serve` or
// `me server`). Defaults to :3000; override when that port is taken, e.g.
// ME_DEV_RPC_TARGET=http://127.0.0.1:3100 ./bun run web
const rpcTarget = process.env.ME_DEV_RPC_TARGET ?? "http://localhost:3000";

/**
* Vite config for the Memory Engine web UI.
*
Expand Down Expand Up @@ -44,11 +49,11 @@ export default defineConfig({
strictPort: false,
proxy: {
"/rpc": {
target: "http://localhost:3000",
target: rpcTarget,
changeOrigin: true,
},
"/healthz": {
target: "http://localhost:3000",
target: rpcTarget,
changeOrigin: true,
},
},
Expand Down
72 changes: 72 additions & 0 deletions scripts/web-remote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env bun
/**
* web:remote — one command for the web UI dev loop against a remote backend.
*
* Spawns two processes and wires them together:
* 1. `me serve` — the credentialed `/rpc` proxy to the remote server (default
* production), on an auto-picked free port. Injects your OAuth token +
* active space; handles token refresh-on-401.
* 2. the Vite dev server (`packages/web`, hot reload) with `ME_DEV_RPC_TARGET`
* pointed at the `me serve` port.
*
* Open http://localhost:5173. Both processes are torn down together on Ctrl+C
* or if either one exits. Override the backend with `ME_SERVER=…`.
*
* This is the single-command form of the two-terminal flow documented in
* DEVELOPMENT.md → "Developing the web UI (hot reload)".
*/
import { join } from "node:path";
import { findAvailablePort } from "../packages/cli/serve/http-server.ts";

const REPO_ROOT = join(import.meta.dir, "..");
const server = process.env.ME_SERVER ?? "https://api.memory.build";
const port = await findAvailablePort("127.0.0.1", 3100, 20);
const target = `http://127.0.0.1:${port}`;

const children: Bun.Subprocess[] = [];
let shuttingDown = false;
function shutdown(code = 0): never {
if (!shuttingDown) {
shuttingDown = true;
for (const child of children) child.kill();
}
process.exit(code);
}
process.on("SIGINT", () => shutdown(0));
process.on("SIGTERM", () => shutdown(0));

console.log(`[web:remote] me serve → ${server} (proxy ${target})`);

// me serve: the auth proxy. stdin ignored so Vite owns the terminal's stdin
// (its 'r'/'q'/'h' shortcuts), and so a stray keystroke can't disturb it.
children.push(
Bun.spawn(
[
process.execPath,
"run",
"packages/cli/index.ts",
"serve",
"--server",
server,
"--port",
String(port),
"--no-open",
],
{ cwd: REPO_ROOT, stdin: "ignore", stdout: "inherit", stderr: "inherit" },
),
);

// Vite dev server: hot reload, proxying /rpc + /healthz to me serve.
children.push(
Bun.spawn([process.execPath, "--filter", "@memory.build/web", "dev"], {
cwd: REPO_ROOT,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
env: { ...process.env, ME_DEV_RPC_TARGET: target },
}),
);

// If either child exits (e.g. `me serve` fails because you're not logged in),
// bring the whole loop down so you don't end up with a half-running setup.
for (const child of children) child.exited.then((code) => shutdown(code ?? 0));