chore: import upstream snapshot with attribution
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:43:05 +08:00
commit 426e9eeabd
41828 changed files with 9656266 additions and 0 deletions
@@ -0,0 +1,68 @@
#!/usr/bin/env node
/**
* Build the local eliza-cloud-agent Docker image so LocalDockerSandboxProvider
* has something to spawn.
*
* bun run --cwd packages/cloud/api agent:build
*
* The build context root is one level above this repo so
* the Dockerfile's `COPY eliza/packages/...` paths resolve. The tag is
* `eliza-cloud-agent:local` to match the default `ELIZA_AGENT_IMAGE` used
* by the local provider.
*/
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// scripts/cloud/admin/dev -> back up to packages, then up to eliza root, then eliza root
const elizaRoot = path.resolve(__dirname, "../../../..");
const elizaRoot = path.resolve(elizaRoot, "..");
const dockerfile = path.resolve(
elizaRoot,
"packages/app-core/deploy/Dockerfile.cloud-agent",
);
if (!existsSync(dockerfile)) {
console.error(`[agent:build] Dockerfile not found at ${dockerfile}`);
process.exit(1);
}
const tag = process.env.ELIZA_AGENT_IMAGE_TAG ?? "eliza-cloud-agent:local";
const platform =
process.env.ELIZA_AGENT_IMAGE_PLATFORM ??
(process.arch === "arm64" ? "linux/arm64" : "linux/amd64");
const dockerfileRelToContext = path.relative(elizaRoot, dockerfile);
console.log(`[agent:build] tag=${tag} platform=${platform}`);
console.log(`[agent:build] context=${elizaRoot}`);
console.log(`[agent:build] dockerfile=${dockerfileRelToContext}`);
const result = spawnSync(
"docker",
[
"build",
"-f",
dockerfileRelToContext,
"-t",
tag,
"--platform",
platform,
".",
],
{
cwd: elizaRoot,
stdio: "inherit",
},
);
if (result.status !== 0) {
console.error(`[agent:build] docker build failed with exit ${result.status}`);
process.exit(result.status ?? 1);
}
console.log(`[agent:build] built ${tag}`);
@@ -0,0 +1,354 @@
#!/usr/bin/env node
// Drives cloud admin cloud admin dev cloud api dev automation with explicit environment and CI invariants.
import { spawn, spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import { createConnection } from "node:net";
import path from "node:path";
import process from "node:process";
const repoRoot = path.resolve(import.meta.dirname, "../../../../..");
const cloudApiDir = path.join(repoRoot, "packages", "cloud", "api");
const require = createRequire(import.meta.url);
const rawArgs = process.argv.slice(2);
const withControlPlane = rawArgs.includes("--with-control-plane");
const args = rawArgs.filter((a) => a !== "--with-control-plane");
const host = process.env.PGLITE_HOST || "127.0.0.1";
const port = Number.parseInt(
process.env.DEV_CLOUD_PGLITE_PORT || process.env.PGLITE_PORT || "55432",
10,
);
const apiPort = process.env.API_DEV_PORT || "8787";
const maxConnections = process.env.PGLITE_MAX_CONNECTIONS || "16";
const startupTimeoutMs = Number.parseInt(
process.env.DEV_CLOUD_STARTUP_TIMEOUT_MS || "120000",
10,
);
const pollIntervalMs = 500;
function bunExecutable() {
if (process.env.BUN && existsSync(process.env.BUN)) return process.env.BUN;
// On Windows, Node can spawn `bun.exe` directly but NOT the extensionless npm
// shim (`spawn ENOENT`) nor a `.cmd` without `shell: true`. Probe the native
// `.exe` first so the npm shim never wins. On POSIX the binary is just `bun`.
const names = process.platform === "win32" ? ["bun.exe", "bun"] : ["bun"];
const home = process.env.HOME || process.env.USERPROFILE || "";
const dirs = [
path.resolve(home, ".bun/bin"),
...(process.env.PATH?.split(path.delimiter) ?? []),
];
for (const dir of dirs) {
for (const name of names) {
const candidate = path.resolve(dir, name);
if (existsSync(candidate)) return candidate;
}
}
if (process.env.npm_execpath?.includes("bun"))
return process.env.npm_execpath;
return process.platform === "win32" ? "bun.exe" : "bun";
}
function isRealNodeExecutable(candidate) {
if (!candidate || !existsSync(candidate)) return false;
const result = spawnSync(
candidate,
["-e", "process.exit(process.versions.bun ? 1 : 0)"],
{ stdio: "ignore" },
);
return result.status === 0;
}
function nodeExecutable() {
const candidates = [
process.env.NODE,
process.execPath,
...(process.env.PATH?.split(path.delimiter).map((entry) =>
path.resolve(entry, "node"),
) ?? []),
"/opt/homebrew/bin/node",
"/usr/local/bin/node",
"/usr/bin/node",
];
const seen = new Set();
for (const candidate of candidates) {
if (!candidate || seen.has(candidate)) continue;
seen.add(candidate);
if (isRealNodeExecutable(candidate)) return candidate;
}
return "node";
}
function wranglerScript() {
// wrangler's package.json `exports` does not expose `bin/wrangler.js` as a
// subpath, so require.resolve("wrangler/bin/wrangler.js") throws
// ERR_PACKAGE_PATH_NOT_EXPORTED on wrangler >=4. Resolve the package via its
// (exported) package.json and read the declared bin path instead.
const pkgJsonPath = require.resolve("wrangler/package.json", {
paths: [cloudApiDir, repoRoot],
});
const pkg = require(pkgJsonPath);
const binRel =
typeof pkg.bin === "string"
? pkg.bin
: (pkg.bin?.wrangler ?? "bin/wrangler.js");
return path.join(path.dirname(pkgJsonPath), binRel);
}
function parsePGliteDataDir(url) {
if (!url?.startsWith("pglite://")) return null;
const dataDir = url.slice("pglite://".length);
if (!dataDir || dataDir === "memory") return null;
return dataDir;
}
function shouldUsePGliteTcpBridge(env) {
const url = env.DATABASE_URL || env.TEST_DATABASE_URL || "";
return !url || url.startsWith("pglite://");
}
async function tcpOk() {
return new Promise((resolve) => {
const socket = createConnection({ host, port });
socket.setTimeout(1000);
socket.once("connect", () => {
socket.end();
resolve(true);
});
socket.once("timeout", () => {
socket.destroy();
resolve(false);
});
socket.once("error", () => {
socket.destroy();
resolve(false);
});
});
}
async function waitForTcp(child) {
const startedAt = Date.now();
while (Date.now() - startedAt < startupTimeoutMs) {
if (await tcpOk()) return;
if (child.exitCode !== null) {
throw new Error(`PGlite TCP server exited with code ${child.exitCode}`);
}
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
throw new Error(
`PGlite TCP server did not become reachable at ${host}:${port}`,
);
}
function runStep(label, command, stepArgs, env) {
const result = spawnSync(command, stepArgs, {
cwd: repoRoot,
env,
stdio: "inherit",
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`${label} exited with code ${result.status}`);
}
}
async function main() {
const bun = bunExecutable();
let pgliteChild = null;
const env = {
...process.env,
NODE_ENV: process.env.NODE_ENV || "development",
API_DEV_PORT: apiPort,
};
if (shouldUsePGliteTcpBridge(env)) {
const configuredUrl = env.DATABASE_URL || env.TEST_DATABASE_URL || "";
const dataDir =
parsePGliteDataDir(configuredUrl) ||
env.DEV_CLOUD_PGLITE_DATA_DIR ||
env.PGLITE_DATA_DIR ||
".eliza/.pgdata";
env.DATABASE_URL = `postgresql://postgres@${host}:${port}/postgres`;
env.TEST_DATABASE_URL ||= env.DATABASE_URL;
if (!(await tcpOk())) {
pgliteChild = spawn(
bun,
["run", "packages/scripts/cloud/admin/dev/pglite-server.ts"],
{
cwd: repoRoot,
env: {
...env,
PGLITE_HOST: host,
PGLITE_PORT: String(port),
PGLITE_MAX_CONNECTIONS: maxConnections,
PGLITE_DATA_DIR: dataDir,
},
stdio: ["ignore", "inherit", "inherit"],
},
);
await waitForTcp(pgliteChild);
}
}
if (env.DEV_CLOUD_SKIP_MIGRATE !== "1") {
runStep("db:cloud:migrate", bun, ["run", "db:cloud:migrate"], env);
}
runStep(
"sync-api-dev-vars",
bun,
["run", "packages/scripts/cloud/admin/sync-api-dev-vars.ts"],
env,
);
// When the e2e harness runs (NODE_ENV=test), forward the test KMS backend via
// `--var`. wrangler's `[vars] NODE_ENV = "production"` block in wrangler.toml
// takes precedence over the shell NODE_ENV, which would otherwise cause
// `resolveKmsBackend()` in `@elizaos/security/kms` to default to the Steward
// backend and throw `KmsError("ELIZA_KMS_BACKEND=steward requires
// steward.{baseUrl, tokenProvider}")` for any route that touches encrypted
// fields. Cross-process e2e flows use the local backend with a deterministic
// root key so Worker routes can decrypt rows written by the control plane.
const isE2eTestMode =
process.env.NODE_ENV === "test" || process.env.CLOUD_E2E === "1";
const kmsBackend = process.env.ELIZA_KMS_BACKEND ?? "memory";
const localRootKey = process.env.ELIZA_LOCAL_ROOT_KEY;
// In e2e/test mode, also stub the Cloudflare registrar/DNS by default so the
// domain buy/check routes never hit the real Cloudflare API (overridable via
// ELIZA_CF_REGISTRAR_DEV_STUB).
const registrarStub = process.env.ELIZA_CF_REGISTRAR_DEV_STUB ?? "1";
// In e2e/test mode, neutralize the dedicated-agent public subdomain. The agent
// detail route synthesizes `web_ui_url = https://<id>.<ELIZA_CLOUD_AGENT_BASE_DOMAIN>`
// (wrangler.toml pins `elizacloud.ai`), and the client's readiness probe prefers
// that web_ui_url over the agent's `bridge_url`. There is no Worker-fronted
// `*.elizacloud.ai` ingress in the local mock stack, so that subdomain is
// unreachable — the dedicated agent's reachable base IS its `bridge_url` (the
// control-plane mock). The detail route feeds `containersEnv.publicBaseDomain()`
// into `getElizaAgentPublicWebUiUrl` as the EXPLICIT `{baseDomain}` option; a
// value that normalizes to empty makes that explicit-option path return null, so
// the reachable bridge wins and the shared→dedicated handoff import can complete.
// (The compat-envelope no-option path keeps its default and is intentionally not
// neutralized.) The apps domain (`CONTAINERS_PUBLIC_BASE_DOMAIN`) is a separate
// knob and is left untouched.
const agentBaseDomainOverride =
process.env.ELIZA_CLOUD_AGENT_BASE_DOMAIN ?? "https://";
const testModeVars = isE2eTestMode
? [
"--var",
"NODE_ENV:test",
"--var",
`ELIZA_KMS_BACKEND:${kmsBackend}`,
...(localRootKey
? ["--var", `ELIZA_LOCAL_ROOT_KEY:${localRootKey}`]
: []),
"--var",
`ELIZA_CF_REGISTRAR_DEV_STUB:${registrarStub}`,
"--var",
`ELIZA_CLOUD_AGENT_BASE_DOMAIN:${agentBaseDomainOverride}`,
]
: [];
// Redis selection must reach the Worker's `c.env`, not just this launcher's
// process.env: routes build their client via `buildRedisClient(c.env)`, and
// wrangler `--local` only exposes vars passed through wrangler.toml/.dev.vars/
// `--var` — ambient process.env does NOT leak into `c.env`. Without this, the
// e2e env's `MOCK_REDIS=1` is invisible inside the Worker and any nonce-storage
// route (e.g. SIWE nonce/verify) returns 503 "Nonce storage unavailable". Most
// redis consumers (rate-limit) fail open, so this only surfaced once a spec
// drove the real SIWE login. Forward whichever redis selector is set.
const redisDevVars = ["MOCK_REDIS", "REDIS_URL"].flatMap((key) => {
const value = process.env[key];
return value ? ["--var", `${key}:${value}`] : [];
});
const appDeployDevVars = [
"APPS_DEPLOY_ENABLED",
"APPS_DEPLOY_ALLOWED_ORG_IDS",
"APP_DEFAULT_IMAGE",
].flatMap((key) => {
const value = process.env[key];
return value ? ["--var", `${key}:${value}`] : [];
});
const wranglerArgs =
args.length > 0
? args
: [
"dev",
"--ip",
"127.0.0.1",
"--port",
apiPort,
"--local",
...testModeVars,
...redisDevVars,
...appDeployDevVars,
];
const useNodeWrangler = env.CLOUD_E2E === "1" && env.NODE_ENV === "test";
const wranglerCmd = useNodeWrangler ? nodeExecutable() : bun;
const wranglerSpawnArgs = useNodeWrangler
? [wranglerScript(), ...wranglerArgs]
: ["run", "wrangler", ...wranglerArgs];
const wrangler = spawn(wranglerCmd, wranglerSpawnArgs, {
cwd: cloudApiDir,
env,
stdio: "inherit",
});
// When --with-control-plane is passed, also boot the container-control-plane
// bun service on :8791 so the cloud-api can forward provisioning jobs to it
// (otherwise provision endpoints succeed but jobs queue forever).
let controlPlane = null;
if (withControlPlane) {
const controlPlaneEnv = {
...env,
// Control-plane reads DATABASE_URL directly (not through dev-vars).
DATABASE_URL:
env.DATABASE_URL || `postgresql://postgres@${host}:${port}/postgres`,
ELIZA_LOCAL_DOCKER_PROVIDER: env.ELIZA_LOCAL_DOCKER_PROVIDER || "1",
ENVIRONMENT: env.ENVIRONMENT || "local",
ELIZA_AGENT_IMAGE: env.ELIZA_AGENT_IMAGE || "eliza-cloud-agent:local",
ELIZA_AGENT_PORT: env.ELIZA_AGENT_PORT || "2138",
ELIZA_AGENT_BRIDGE_PORT: env.ELIZA_AGENT_BRIDGE_PORT || "18790",
NEXT_PUBLIC_API_URL:
env.NEXT_PUBLIC_API_URL || `http://127.0.0.1:${apiPort}`,
};
console.log("[cloud-api-dev] starting container-control-plane on :8791");
controlPlane = spawn(bun, ["run", "start"], {
cwd: path.join(
repoRoot,
"packages",
"cloud-services",
"container-control-plane",
),
env: controlPlaneEnv,
stdio: "inherit",
});
controlPlane.on("exit", (code) => {
console.warn(`[cloud-api-dev] control-plane exited (code ${code})`);
});
}
const shutdown = () => {
wrangler.kill("SIGTERM");
controlPlane?.kill("SIGTERM");
pgliteChild?.kill("SIGTERM");
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
wrangler.on("exit", (code, signal) => {
pgliteChild?.kill("SIGTERM");
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 0);
});
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
@@ -0,0 +1,178 @@
#!/usr/bin/env node
// Drives cloud admin cloud admin dev cloud api e2e server automation with explicit environment and CI invariants.
import { createServer } from "node:http";
import path from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { pathToFileURL } from "node:url";
const repoRoot = path.resolve(import.meta.dirname, "../../../../..");
const host = process.env.API_DEV_HOST || "127.0.0.1";
const port = Number.parseInt(process.env.API_DEV_PORT || "8787", 10);
function asArrayBuffer(value) {
if (value instanceof ArrayBuffer) return value;
if (ArrayBuffer.isView(value)) {
return value.buffer.slice(
value.byteOffset,
value.byteOffset + value.byteLength,
);
}
if (typeof value === "string") return new TextEncoder().encode(value).buffer;
if (value === null || value === undefined) return new ArrayBuffer(0);
throw new TypeError(`Unsupported R2 test object value: ${typeof value}`);
}
function createMemoryR2Bucket() {
const objects = new Map();
return {
async get(key) {
const object = objects.get(key);
if (!object) return null;
return {
httpMetadata: object.httpMetadata,
customMetadata: object.customMetadata,
async text() {
return new TextDecoder().decode(object.body);
},
async arrayBuffer() {
return object.body.slice(0);
},
};
},
async put(key, value, options = {}) {
const body =
value instanceof Blob
? await value.arrayBuffer()
: asArrayBuffer(value);
objects.set(key, {
body,
httpMetadata: options.httpMetadata ?? {},
customMetadata: options.customMetadata ?? {},
});
return { key };
},
async delete(key) {
objects.delete(key);
},
};
}
function createExecutionContext() {
const pending = [];
return {
passThroughOnException() {},
waitUntil(promise) {
pending.push(Promise.resolve(promise));
},
async drain() {
while (pending.length > 0) {
const batch = pending.splice(0);
await Promise.allSettled(batch);
}
},
};
}
const HOP_BY_HOP_HEADERS = new Set([
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
]);
function filteredRequestHeaders(incomingHeaders) {
const headers = new Headers();
const connectionHeader = incomingHeaders.connection;
const connectionTokens = new Set(
(Array.isArray(connectionHeader)
? connectionHeader.join(",")
: (connectionHeader ?? "")
)
.split(",")
.map((token) => token.trim().toLowerCase())
.filter(Boolean),
);
for (const [name, value] of Object.entries(incomingHeaders)) {
const lowerName = name.toLowerCase();
if (HOP_BY_HOP_HEADERS.has(lowerName) || connectionTokens.has(lowerName)) {
continue;
}
if (Array.isArray(value)) {
for (const item of value) {
headers.append(name, item);
}
} else if (typeof value === "string") {
headers.set(name, value);
}
}
return headers;
}
const workerUrl = pathToFileURL(
path.join(repoRoot, "packages/cloud/api/src/index.ts"),
).href;
const worker = (await import(workerUrl)).default;
const env = {
...process.env,
API_DEV_PORT: String(port),
BLOB: createMemoryR2Bucket(),
};
const server = createServer(async (incoming, outgoing) => {
try {
const requestUrl = new URL(
incoming.url ?? "/",
`http://${incoming.headers.host ?? `${host}:${port}`}`,
);
const method = incoming.method ?? "GET";
const request = new Request(requestUrl, {
method,
headers: filteredRequestHeaders(incoming.headers),
body:
method === "GET" || method === "HEAD"
? undefined
: Readable.toWeb(incoming),
duplex: "half",
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await ctx.drain();
outgoing.writeHead(
response.status,
response.statusText,
Object.fromEntries(response.headers),
);
if (response.body) {
await pipeline(Readable.fromWeb(response.body), outgoing);
} else {
outgoing.end();
}
} catch (error) {
console.error("[cloud-api-e2e] request failed", error);
if (outgoing.headersSent) {
outgoing.destroy(error instanceof Error ? error : undefined);
return;
}
outgoing.writeHead(500, { "Content-Type": "application/json" });
outgoing.end(JSON.stringify({ error: "cloud-api-e2e request failed" }));
}
});
await new Promise((resolve) => {
server.listen(port, host, resolve);
});
console.log(`[cloud-api-e2e] listening on http://${host}:${port}`);
const shutdown = () => {
server.close(() => process.exit(0));
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
await new Promise(() => undefined);
@@ -0,0 +1,256 @@
#!/usr/bin/env bun
/**
* Local Hono server for the mock cloud E2E harness.
*
* The production Cloud API still runs as a Cloudflare Worker. This launcher is
* intentionally scoped to local/mock tests where we need deterministic process
* startup and the same Hono route graph, but not Wrangler's dev proxy/runtime.
*/
import { createApp } from "../../../../cloud/api/src/bootstrap-app";
type StoredObject = {
bytes: Uint8Array;
httpMetadata?: { contentType?: string };
customMetadata?: Record<string, string>;
uploaded: Date;
etag: string;
};
const encoder = new TextEncoder();
const store = new Map<string, StoredObject>();
const multipartUploads = new Map<
string,
{
key: string;
httpMetadata?: { contentType?: string };
customMetadata?: Record<string, string>;
parts: Map<number, Uint8Array>;
}
>();
async function toBytes(
value: string | ArrayBuffer | ArrayBufferView | Blob | null,
): Promise<Uint8Array> {
if (value === null) return new Uint8Array();
if (typeof value === "string") return encoder.encode(value);
if (value instanceof ArrayBuffer) return new Uint8Array(value);
if (ArrayBuffer.isView(value)) {
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
}
return new Uint8Array(await value.arrayBuffer());
}
function createEtag(bytes: Uint8Array): string {
return `"local-${bytes.byteLength}-${Bun.hash(bytes)}"`;
}
function objectHead(key: string, object: StoredObject) {
return {
key,
version: null,
size: object.bytes.byteLength,
etag: object.etag,
httpEtag: object.etag,
uploaded: object.uploaded,
httpMetadata: object.httpMetadata,
customMetadata: object.customMetadata,
checksums: {},
};
}
const blobBinding = {
async get(key: string) {
const object = store.get(key);
if (!object) return null;
return {
...objectHead(key, object),
httpMetadata: object.httpMetadata,
customMetadata: object.customMetadata,
async text() {
return new TextDecoder().decode(object.bytes);
},
async arrayBuffer() {
return object.bytes.buffer.slice(
object.bytes.byteOffset,
object.bytes.byteOffset + object.bytes.byteLength,
);
},
};
},
async put(
key: string,
value: string | ArrayBuffer | ArrayBufferView | Blob | null,
options?: {
httpMetadata?: { contentType?: string };
customMetadata?: Record<string, string>;
},
) {
const bytes = await toBytes(value);
store.set(key, {
bytes,
httpMetadata: options?.httpMetadata,
customMetadata: options?.customMetadata,
uploaded: new Date(),
etag: createEtag(bytes),
});
},
async delete(key: string) {
store.delete(key);
},
async list(options?: { prefix?: string; limit?: number; cursor?: string }) {
const prefix = options?.prefix ?? "";
const start = options?.cursor ? Number.parseInt(options.cursor, 10) : 0;
const limit = Math.max(1, options?.limit ?? 1000);
const matchingKeys = Array.from(store.keys())
.filter((key) => key.startsWith(prefix))
.sort();
const page = matchingKeys.slice(start, start + limit);
const next = start + page.length;
const objects = page.flatMap((key) => {
const object = store.get(key);
return object ? [objectHead(key, object)] : [];
});
return {
objects,
truncated: next < matchingKeys.length,
cursor: next < matchingKeys.length ? String(next) : undefined,
delimitedPrefixes: [],
};
},
async head(key: string) {
const object = store.get(key);
return object ? objectHead(key, object) : null;
},
async createMultipartUpload(
key: string,
options?: {
httpMetadata?: { contentType?: string };
customMetadata?: Record<string, string>;
},
) {
const uploadId = crypto.randomUUID();
multipartUploads.set(uploadId, {
key,
httpMetadata: options?.httpMetadata,
customMetadata: options?.customMetadata,
parts: new Map(),
});
return this.resumeMultipartUpload(key, uploadId);
},
resumeMultipartUpload(key: string, uploadId: string) {
const upload = multipartUploads.get(uploadId);
if (!upload || upload.key !== key) {
throw new Error(
`[cloud-api-hono-dev] multipart upload not found: ${key} ${uploadId}`,
);
}
return {
key,
uploadId,
async uploadPart(
partNumber: number,
value: string | ArrayBuffer | ArrayBufferView | Blob,
) {
const bytes = await toBytes(value);
upload.parts.set(partNumber, bytes);
return {
partNumber,
etag: createEtag(bytes),
};
},
async complete(uploadedParts: Array<{ partNumber: number }>) {
const orderedParts = uploadedParts
.map((part) => upload.parts.get(part.partNumber))
.filter((part): part is Uint8Array => part !== undefined);
const totalBytes = orderedParts.reduce(
(total, part) => total + part.byteLength,
0,
);
const bytes = new Uint8Array(totalBytes);
let offset = 0;
for (const part of orderedParts) {
bytes.set(part, offset);
offset += part.byteLength;
}
store.set(key, {
bytes,
httpMetadata: upload.httpMetadata,
customMetadata: upload.customMetadata,
uploaded: new Date(),
etag: createEtag(bytes),
});
multipartUploads.delete(uploadId);
const completedObject = store.get(key);
if (!completedObject) {
throw new Error(
`[cloud-api-hono-dev] multipart completion failed: ${key}`,
);
}
return objectHead(key, completedObject);
},
async abort() {
multipartUploads.delete(uploadId);
},
};
},
};
function executionContext(): ExecutionContext {
return {
waitUntil(promise) {
Promise.resolve(promise).catch((error) => {
console.error("[cloud-api-hono-dev] waitUntil failed", error);
});
},
passThroughOnException() {},
} as ExecutionContext;
}
const port = Number.parseInt(process.env.API_DEV_PORT || "8787", 10);
const hostname = process.env.API_DEV_HOST || "127.0.0.1";
const app = createApp();
const env = {
...process.env,
BLOB: blobBinding,
};
const server = Bun.serve({
hostname,
port,
async fetch(request) {
try {
const url = new URL(request.url);
if (url.pathname === "/api/health") {
return Response.json(
{
status: "ok",
timestamp: Date.now(),
region: "local-hono",
},
{ headers: { "Cache-Control": "no-store, max-age=0" } },
);
}
return await app.fetch(request, env, executionContext());
} catch (error) {
console.error("[cloud-api-hono-dev] unhandled request error", error);
return Response.json(
{ success: false, error: "internal_error" },
{ status: 500 },
);
}
},
});
console.log(
`[cloud-api-hono-dev] listening on http://${hostname}:${server.port}`,
);
const shutdown = () => {
server.stop(true);
process.exit(0);
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
@@ -0,0 +1,80 @@
#!/usr/bin/env bun
/**
* PGlite TCP server for local development.
*
* Boots an embedded PGlite instance with pgvector and exposes it on a
* Postgres-compatible TCP socket so the wrangler/Miniflare API and any other
* `pg`-style consumer can connect with no Docker. One process per workspace.
*
* bun run pglite:server # default :5432, .eliza/.pgdata
* PGLITE_PORT=55432 bun run pglite:server
* PGLITE_DATA_DIR=/tmp/eliza-pglite bun run pglite:server
* PGLITE_IN_MEMORY=1 bun run pglite:server
*/
import { mkdirSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
const PORT = Number.parseInt(process.env.PGLITE_PORT ?? "5432", 10);
const HOST = process.env.PGLITE_HOST ?? "127.0.0.1";
const MAX_CONNECTIONS = Number.parseInt(
process.env.PGLITE_MAX_CONNECTIONS ?? "16",
10,
);
const DATA_DIR =
process.env.PGLITE_IN_MEMORY === "1"
? undefined
: path.resolve(
process.cwd(),
process.env.PGLITE_DATA_DIR ?? ".eliza/.pgdata",
);
const tag = "[pglite]";
if (DATA_DIR) {
mkdirSync(DATA_DIR, { recursive: true });
}
const requireFromCwd = createRequire(path.join(process.cwd(), "package.json"));
const [{ PGlite }, { vector }, { PGLiteSocketServer }] = await Promise.all([
import(requireFromCwd.resolve("@electric-sql/pglite")),
import(requireFromCwd.resolve("@electric-sql/pglite/vector")),
import(requireFromCwd.resolve("@electric-sql/pglite-socket")),
]);
const db = await PGlite.create({
dataDir: DATA_DIR,
extensions: { vector },
});
const server = new PGLiteSocketServer({
db,
port: PORT,
host: HOST,
maxConnections: MAX_CONNECTIONS,
debug: process.env.PGLITE_DEBUG === "1",
inspect: process.env.PGLITE_INSPECT === "1",
});
await server.start();
console.log(
`${tag} listening on ${HOST}:${PORT} (${DATA_DIR ? `data: ${DATA_DIR}` : "in-memory"})`,
);
console.log(`${tag} max connections: ${MAX_CONNECTIONS}`);
console.log(
`${tag} DATABASE_URL=postgresql://postgres@${HOST}:${PORT}/postgres`,
);
async function shutdown(signal: string) {
console.log(`${tag} ${signal} — closing server`);
// error-policy:J6 best-effort teardown on shutdown signal; process exits regardless
await server.stop().catch(() => {});
await db.close().catch(() => {});
process.exit(0);
}
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));