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
22 changes: 0 additions & 22 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,6 @@
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
},
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/scripts/bootstrap.sh",
"timeout": 120000
}
]
},
{
"matcher": "resume",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/scripts/bootstrap.sh",
"timeout": 120000
}
]
}
],
"TaskCompleted": [
{
"matcher": "",
Expand Down
67 changes: 67 additions & 0 deletions .conductor/cloud-init.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
#
# Conductor Cloud snapshot initialization script.
#
# Runs ONCE when Conductor builds the cloud snapshot (Vercel Sandbox / Amazon
# Linux 2023); its output is baked into the snapshot, so every workspace forked
# from it gets bun, PostgreSQL, and Redis pre-installed for free.
#
# This is the slow, repeatable, system-level work. Per-workspace runtime work
# (starting the services, creating databases, writing .env files) lives in
# .conductor/setup.ts, which runs on every workspace creation.
#
# To wire it up: in the Conductor app, go to Settings -> Cloud -> Snapshots and
# set the initialization script to run this file (then "Build snapshot now"):
#
# bash .conductor/cloud-init.sh
# bun install --frozen-lockfile --prefer-offline || bun install
#
set -euo pipefail

echo "=== Conductor Cloud init: installing bun, PostgreSQL, Redis ==="

# -----------------------------------------------------------------------------
# bun
# -----------------------------------------------------------------------------
# Install to $HOME/.bun, then symlink into ~/.local/bin (already on PATH and
# user-owned). We do NOT touch $PATH directly — Conductor reserves it.
if ! command -v bun >/dev/null 2>&1; then
echo "[bun] installing..."
export BUN_INSTALL="$HOME/.bun"
curl -fsSL https://bun.sh/install | bash
mkdir -p "$HOME/.local/bin"
ln -sf "$BUN_INSTALL/bin/bun" "$HOME/.local/bin/bun"
ln -sf "$BUN_INSTALL/bin/bunx" "$HOME/.local/bin/bunx"
else
echo "[bun] already installed ($(command -v bun))"
fi

# -----------------------------------------------------------------------------
# PostgreSQL + Redis (Amazon Linux 2023 packages)
# -----------------------------------------------------------------------------
# postgresql16 client binaries land on /usr/bin (psql, createdb, pg_isready);
# postgresql16-server adds initdb/pg_ctl/postgres. redis6 installs its binaries
# as redis6-server / redis6-cli, so we symlink the conventional names too.
echo "[dnf] installing postgresql16, postgresql16-server, redis6..."
sudo dnf install -y postgresql16 postgresql16-server redis6

mkdir -p "$HOME/.local/bin"
ln -sf /usr/bin/redis6-server "$HOME/.local/bin/redis-server"
ln -sf /usr/bin/redis6-cli "$HOME/.local/bin/redis-cli"

# -----------------------------------------------------------------------------
# Initialize the PostgreSQL data directory
# -----------------------------------------------------------------------------
# A user-owned data dir + trust auth sidesteps the postgres-system-user / peer
# auth dance. The "postgres" role is created here so DATABASE_URLs of the form
# postgres://postgres@localhost work. This dir is baked into the snapshot, so
# initdb runs at most once.
PGDATA="$HOME/pgdata"
if [ ! -d "$PGDATA/base" ]; then
echo "[postgres] initializing data dir at $PGDATA..."
/usr/bin/initdb -D "$PGDATA" -U postgres --auth=trust
else
echo "[postgres] data dir already initialized at $PGDATA"
fi

echo "=== Conductor Cloud init complete ==="
8 changes: 8 additions & 0 deletions .conductor/settings.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ archive = "bun .conductor/teardown.ts"
run = "bun dev"
run_mode = "nonconcurrent"
setup = "bun install && bun .conductor/setup.ts"

# Cloud-only env vars so terminals and agents inherit the Redis URLs without
# sourcing a file. DATABASE_URL* stay in each workspace's .env (written by
# .conductor/setup.ts) so the framework package and example backend use
# isolated test databases.
[environment_variables.cloud]
REDIS_URL = "redis://localhost:6379/0"
REDIS_URL_TEST = "redis://localhost:6379/1"
143 changes: 143 additions & 0 deletions .conductor/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ import { join } from "path";

const ROOT_DIR = join(import.meta.dirname, "..");

// Conductor Cloud (Vercel Sandbox / Amazon Linux 2023) takes a dedicated path:
// the toolchain is installed by .conductor/cloud-init.sh at snapshot build time,
// and here we just start the services, create databases, and write .env files.
// CONDUCTOR_IS_LOCAL is "0" in cloud workspaces and "1" locally.
if (process.env.CONDUCTOR_IS_LOCAL === "0") {
await setupCloud();
process.exit(0);
}

const workspaceName = process.env.CONDUCTOR_WORKSPACE_NAME;
const conductorPort = process.env.CONDUCTOR_PORT
? parseInt(process.env.CONDUCTOR_PORT)
Expand Down Expand Up @@ -207,3 +216,137 @@ for (const [key, val] of Object.entries(frontendOverrides)) {
}

console.log("\nSetup complete! Run 'bun dev' to start both servers.");

/**
* Conductor Cloud setup (Amazon Linux 2023).
*
* The toolchain (bun, PostgreSQL, Redis) is installed at snapshot-build time by
* .conductor/cloud-init.sh. Here we do the per-workspace runtime work that can't
* be baked into a snapshot: start the services, create the databases, and write
* the .env files. Cloud workspaces are isolated sandboxes, so we use fixed ports
* and database names (no per-workspace hashing like the local path).
*
* Why start the services here instead of assuming they're up at boot? A snapshot
* captures the filesystem, not running processes, so anything started during
* cloud-init is gone when a workspace forks the snapshot. And the sandbox isn't
* systemd-managed (PID 1 is Vercel's sandbox-init; `systemctl is-system-running`
* is "offline"), so there's no boot-time service manager to `enable` against.
* The setup script — which Conductor runs on every workspace creation — is the
* intended hook for starting services. It's idempotent, so re-runs are cheap.
*
* Binaries are referenced by absolute path because Bun's `$` shell doesn't
* re-read process.env.PATH. Redis ships as redis6-server / redis6-cli on AL2023.
*/
async function setupCloud(): Promise<void> {
console.log("Conductor Cloud detected — running cloud setup.");

const home = process.env.HOME ?? "/home/vercel-sandbox";
const pgData = join(home, "pgdata");

const pgCtl = "/usr/bin/pg_ctl";
const pgIsReady = "/usr/bin/pg_isready";
const createdbBin = "/usr/bin/createdb";
const psqlBin = "/usr/bin/psql";
const redisServer = "/usr/bin/redis6-server";
const redisCli = "/usr/bin/redis6-cli";

const isPgReady = async () =>
(await $`${{ raw: pgIsReady }} -q`.quiet().nothrow()).exitCode === 0;

// Start Postgres (idempotent)
if (await isPgReady()) {
console.log("Postgres is already running.");
} else {
console.log("Starting Postgres...");
await $`${{ raw: pgCtl }} -D ${pgData} -l ${join(pgData, "server.log")} start`.nothrow();
for (let i = 0; i < 30; i++) {
if (await isPgReady()) break;
await Bun.sleep(500);
}
console.log(
(await isPgReady()) ? "Postgres started." : "WARNING: Postgres not ready.",
);
}

const redisPongs = async () => {
const r = await $`${{ raw: redisCli }} ping`.quiet().nothrow();
return r.exitCode === 0 && r.stdout.toString().includes("PONG");
};

// Start Redis (idempotent)
if (await redisPongs()) {
console.log("Redis is already running.");
} else {
console.log("Starting Redis...");
await $`${{ raw: redisServer }} --daemonize yes`.nothrow();
for (let i = 0; i < 20; i++) {
if (await redisPongs()) break;
await Bun.sleep(500);
}
console.log(
(await redisPongs()) ? "Redis started." : "WARNING: Redis not ready.",
);
}

// Create databases. keryx-package-test is isolated from keryx-test so the
// framework package and example backend don't clobber each other's migrations
// (mirrors CI, where each job gets its own Postgres).
for (const db of ["keryx", "keryx-test", "keryx-package-test"]) {
const exists =
await $`${{ raw: psqlBin }} -U postgres -lqt 2>/dev/null | cut -d'|' -f1 | grep -qw ${db}`.nothrow();
if (exists.exitCode === 0) {
console.log(`Database '${db}' already exists.`);
} else {
const created = await $`${{ raw: createdbBin }} -U postgres ${db}`.nothrow();
console.log(
created.exitCode === 0
? `Created database '${db}'.`
: `WARNING: could not create database '${db}'.`,
);
}
}

// Cloud connection strings. Postgres uses trust auth on localhost (set up by
// initdb in cloud-init.sh), so no password is needed.
const dbUrl = (name: string) => `"postgres://postgres@localhost:5432/${name}"`;
const redisUrl = `"redis://localhost:6379/0"`;
const redisUrlTest = `"redis://localhost:6379/1"`;

// Framework + plugins use the isolated package test DB.
const packageOverrides = {
DATABASE_URL: dbUrl("keryx"),
DATABASE_URL_TEST: dbUrl("keryx-package-test"),
REDIS_URL: redisUrl,
REDIS_URL_TEST: redisUrlTest,
};

const writeCloudEnv = (dir: string, overrides: Record<string, string>) => {
const examplePath = join(dir, ".env.example");
if (!existsSync(examplePath)) return;
applyEnvOverrides(examplePath, join(dir, ".env"), overrides);
console.log(`Wrote ${dir.replace(`${ROOT_DIR}/`, "")}/.env`);
};

const packagesDir = join(ROOT_DIR, "packages");
for (const pkg of readdirSync(packagesDir)) {
const pkgDir = join(packagesDir, pkg);
writeCloudEnv(pkgDir, packageOverrides);
if (pkg === "plugins") {
for (const plugin of readdirSync(pkgDir)) {
writeCloudEnv(join(pkgDir, plugin), packageOverrides);
}
}
}

// example/backend uses its own test DB, separate from the framework package's.
writeCloudEnv(join(ROOT_DIR, "example/backend"), {
...packageOverrides,
DATABASE_URL_TEST: dbUrl("keryx-test"),
});

writeCloudEnv(join(ROOT_DIR, "example/frontend"), {
VITE_API_URL: "http://localhost:8080",
});

console.log("\nCloud setup complete! Run 'bun dev' to start both servers.");
}
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,10 @@ If code changes aren't reflected in HTTP responses, check for stale `bun keryx`
ps aux | grep "bun keryx" | grep -v grep
kill -9 <PIDs>
```

## Gotcha: Cloud Postgres/Redis Stop on Resume

In Conductor Cloud workspaces, Postgres and Redis are started by `.conductor/setup.ts` (the cloud branch, taken when `CONDUCTOR_IS_LOCAL=0`), which Conductor runs only at workspace *creation*. The sandbox has no service manager (PID 1 is Vercel's `sandbox-init`, not systemd), so nothing auto-restarts them at boot. If a long-lived workspace is paused and resumed and the OS reaps those processes, tests/`bun dev` will fail to connect. Re-run setup to bring them back (it's idempotent):
```bash
bun .conductor/setup.ts
```
2 changes: 1 addition & 1 deletion packages/keryx/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "keryx",
"version": "0.31.0",
"version": "0.31.1",
"module": "index.ts",
"type": "module",
"license": "MIT",
Expand Down
Loading
Loading