diff --git a/examples/ai-and-user-generated-actors-freestyle/README.md b/examples/ai-and-user-generated-actors-freestyle/README.md new file mode 100644 index 0000000000..2ea03a4bb3 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/README.md @@ -0,0 +1,88 @@ +# User and AI Generated Rivet Actors Freestyle Deployer + +Shows how to deploy user or AI-generated Rivet Actor code using a sandboxed namespace and Freestyle + +[View Full Documentation](https://rivet.dev/docs/actors/ai-and-user-generated-actors) + +## Features + +- **Sandboxed Rivet namespace**: Create isolated environments for testing and deploying actors +- **Deploy user or AI generated code**: Deploy custom actor and frontend code directly to Freestyle +- **Automatic configuration**: Configure Rivet & Freestyle together automatically without manual setup + +## How It Works + +The logic lives in `src/backend/`: + +1. **Receives user or AI-generated code**: The backend receives custom actor code (`registry.ts`) and frontend code (`App.tsx`) +2. **Creates a sandboxed Rivet namespace**: Either using Rivet Cloud or Rivet self-hosted API (see [`deploy-with-rivet-cloud.ts`](src/backend/deploy-with-rivet-cloud.ts) and [`deploy-with-rivet-self-hosted.ts`](src/backend/deploy-with-rivet-self-hosted.ts)) +3. **Deploys actor and frontend to Freestyle**: Builds the project and deploys to Freestyle (see [`src/backend/utils.ts`](src/backend/utils.ts)) +4. **Configures Rivet to connect to Freestyle**: Sets up the Rivet runner to point to the Rivet deployment for running the actors + +## Getting Started + +### Prerequisites + +- Node.js 18+ and pnpm +- A [Freestyle](https://freestyle.sh) account and API key +- Either: + - [Rivet Cloud](https://dashboard.rivet.dev/) account with API token, or + - [Self-hosted Rivet instance](https://www.rivet.dev/docs/self-hosting/) with endpoint and token + +### Installation + +```bash +pnpm install +``` + +### Running the Application + +Start both the frontend and backend servers: + +```bash +pnpm dev +``` + +Open http://localhost:5173 in your browser. + +## Usage + +### Rivet Cloud + +1. **Get Your Cloud API Token**: + - Go to or create your project on [Rivet Cloud](https://dashboard.rivet.dev/) + - Click on "Tokens" in the sidebar + - Under "Cloud API Tokens" click "Create Token" and copy the token +2. **Edit Code**: Modify the `registry.ts` and `App.tsx` code in the editors +3. **Configure Deploy Config**: Fill in all required fields: + - Rivet Cloud API token (from step 1) + - Freestyle domain (e.g., myapp.style.dev) + - Freestyle API key +4. **Deploy**: Click "Deploy to Freestyle" and watch the deployment logs + +### Rivet Self-Hosted + +1. **Edit Code**: Modify the `registry.ts` and `App.tsx` code in the editors +2. **Configure Deploy Config**: Fill in all required fields: + - Rivet endpoint (your self-hosted instance URL) + - Rivet API token + - Freestyle domain (e.g., myapp.style.dev) + - Freestyle API key +3. **Deploy**: Click "Deploy to Freestyle" and watch the deployment logs + +## Project Structure + +``` +src/ + backend/ # Backend used to deploy your sandboxed Rivet backend code + frontend/ # Frontend for the deploy UI you'll be using +template/ # The Rivet template code to deploy with Rivet + src/ + backend/ # Actor code to be deployed + frontend/ # Frontend to be deployed +tests/ # Vitest tests +``` + +## License + +MIT diff --git a/examples/freestyle/package.json b/examples/ai-and-user-generated-actors-freestyle/package.json similarity index 51% rename from examples/freestyle/package.json rename to examples/ai-and-user-generated-actors-freestyle/package.json index 2fc8bbb365..54ae8d2b57 100644 --- a/examples/freestyle/package.json +++ b/examples/ai-and-user-generated-actors-freestyle/package.json @@ -1,38 +1,39 @@ { - "name": "example-freestyle", - "version": "2.0.21", + "name": "example-ai-and-user-generated-actors-freestyle", + "version": "1.0.0", "private": true, "type": "module", "scripts": { "dev": "concurrently \"pnpm run dev:backend\" \"pnpm run dev:frontend\"", - "dev:backend": "deno run --watch --allow-all src/backend/server.ts", + "dev:backend": "tsx src/backend/index.ts", "dev:frontend": "vite", - "freestyle:deploy": "tsx scripts/freestyle-deploy.ts", - "check-types": "tsc --noEmit" + "build": "tsc && vite build", + "preview": "vite preview", + "check-types": "tsc --noEmit", + "test": "vitest", + "test:run": "vitest run" }, - "devDependencies": { + "dependencies": { + "@hono/node-server": "^1.13.8", + "@monaco-editor/react": "^4.7.0", + "@rivet-gg/cloud": "https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@715f221", "@rivetkit/engine-api-full": "^25.7.2", - "@rivetkit/react": "workspace:*", + "dotenv": "^17.2.2", + "execa": "^9.5.2", + "freestyle-sandboxes": "^0.0.95", + "hono": "^4.6.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { "@types/node": "^22.13.9", - "@types/prompts": "^2", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.2.0", "concurrently": "^8.2.2", - "dotenv": "^17.2.2", - "freestyle-sandboxes": "^0.0.95", - "prompts": "^2.4.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "tsup": "^8.5.0", "tsx": "^3.12.7", "typescript": "^5.5.2", "vite": "^5.0.0", - "vitest": "^3.1.1" - }, - "dependencies": { - "hono": "4.9.8", - "rivetkit": "workspace:*" - }, - "stableVersion": "0.8.0" + "vitest": "^2.1.8" + } } diff --git a/examples/ai-and-user-generated-actors-freestyle/src/backend/deploy-with-rivet-cloud.ts b/examples/ai-and-user-generated-actors-freestyle/src/backend/deploy-with-rivet-cloud.ts new file mode 100644 index 0000000000..2c81d58b53 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/src/backend/deploy-with-rivet-cloud.ts @@ -0,0 +1,109 @@ +import { RivetClient as CloudClient } from "@rivet-gg/cloud"; +import { RivetClient } from "@rivetkit/engine-api-full"; +import { + deployToFreestyle, + configureRivetServerless, + generateNamespaceName, + type DeployRequest, + type LogCallback, +} from "./utils"; + +export async function deployWithRivetCloud(req: DeployRequest, log: LogCallback) { + if (!("cloud" in req.kind)) { + throw new Error("Expected cloud deployment request"); + } + + const { cloudEndpoint: apiUrl, cloudToken: apiToken, engineEndpoint: endpoint } = req.kind.cloud; + const { freestyleDomain, freestyleApiKey } = req; + + const cloudRivet = new CloudClient({ + baseUrl: apiUrl, + token: apiToken, + }); + + const { project, organization } = await cloudRivet.apiTokens.inspect(); + + const namespaceName = generateNamespaceName(); + + await log(`Creating namespace ${namespaceName}`); + const { namespace } = await cloudRivet.namespaces.create( + project, + { + name: namespaceName, + displayName: namespaceName.substring(0, 16), + org: organization, + }, + ); + + const { token: runnerToken } = await cloudRivet.namespaces.createSecretToken( + project, + namespace.name, + { + name: `${namespaceName}-runner-token`, + org: organization, + }, + ); + + const { token: publishableToken } = + await cloudRivet.namespaces.createPublishableToken( + project, + namespace.name, + { + org: organization, + }, + ); + + const { token: accessToken } = await cloudRivet.namespaces.createAccessToken( + project, + namespace.name, + { + org: organization, + }, + ); + + const datacenter = req.datacenter || "us-west-1"; + + // Deploy to Freestyle + const { deploymentId } = await deployToFreestyle({ + registryCode: req.registryCode, + appCode: req.appCode, + domain: freestyleDomain, + apiKey: freestyleApiKey, + envVars: { + VITE_RIVET_ENDPOINT: endpoint, + VITE_RIVET_NAMESPACE: namespace.access.engineNamespaceName, + VITE_RIVET_TOKEN: publishableToken, + VITE_RIVET_DATACENTER: datacenter, + RIVET_ENDPOINT: endpoint, + RIVET_NAMESPACE: namespace.access.engineNamespaceName, + RIVET_RUNNER_TOKEN: runnerToken, + RIVET_PUBLISHABLE_TOKEN: publishableToken, + }, + log, + }); + + // Update runner config + const engineRivet = new RivetClient({ + environment: endpoint, + token: accessToken, + }); + + await configureRivetServerless({ + rivet: engineRivet, + domain: freestyleDomain, + namespace: namespace.access.engineNamespaceName, + datacenter, + log, + }); + + return { + success: true, + dashboardUrl: `https://dashboard.rivet.dev/orgs/${organization}/projects/${project}/ns/${namespace.name}`, + freestyleUrl: `https://admin.freestyle.sh/dashboard/deployments/${deploymentId}`, + tokens: { + runnerToken, + publishableToken, + accessToken, + }, + }; +} diff --git a/examples/ai-and-user-generated-actors-freestyle/src/backend/deploy-with-rivet-self-hosted.ts b/examples/ai-and-user-generated-actors-freestyle/src/backend/deploy-with-rivet-self-hosted.ts new file mode 100644 index 0000000000..a9415816b2 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/src/backend/deploy-with-rivet-self-hosted.ts @@ -0,0 +1,64 @@ +import { RivetClient } from "@rivetkit/engine-api-full"; +import { + deployToFreestyle, + configureRivetServerless, + generateNamespaceName, + type DeployRequest, + type LogCallback, +} from "./utils"; + +export async function deployWithRivetSelfHosted(req: DeployRequest, log: LogCallback) { + if (!("selfHosted" in req.kind)) { + throw new Error("Expected self-hosted deployment request"); + } + + const { endpoint, token } = req.kind.selfHosted; + const { freestyleDomain, freestyleApiKey } = req; + + const rivet = new RivetClient({ + environment: endpoint, + token: token, + }); + + const namespaceName = generateNamespaceName(); + + await log(`Creating namespace ${namespaceName}`); + const { namespace } = await rivet.namespaces.create({ + displayName: namespaceName, + name: namespaceName, + }); + + const datacenter = req.datacenter || "us-west-1"; + + // Deploy to Freestyle + const { deploymentId } = await deployToFreestyle({ + registryCode: req.registryCode, + appCode: req.appCode, + domain: freestyleDomain, + apiKey: freestyleApiKey, + envVars: { + VITE_RIVET_ENDPOINT: endpoint, + VITE_RIVET_NAMESPACE: namespace.name, + VITE_RIVET_TOKEN: token, + VITE_RIVET_DATACENTER: datacenter, + RIVET_ENDPOINT: endpoint, + RIVET_NAMESPACE: namespace.name, + RIVET_RUNNER_TOKEN: token, + RIVET_PUBLISHABLE_TOKEN: token, + }, + log, + }); + + await configureRivetServerless({ + rivet, + domain: freestyleDomain, + namespace: namespace.name, + datacenter, + log, + }); + + return { + success: true, + freestyleUrl: `https://admin.freestyle.sh/dashboard/deployments/${deploymentId}`, + }; +} diff --git a/examples/ai-and-user-generated-actors-freestyle/src/backend/index.ts b/examples/ai-and-user-generated-actors-freestyle/src/backend/index.ts new file mode 100644 index 0000000000..810bf98deb --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/src/backend/index.ts @@ -0,0 +1,61 @@ +import { Hono } from "hono"; +import { streamSSE } from "hono/streaming"; +import { logger } from "hono/logger"; +import { serve } from "@hono/node-server"; +import { deployWithRivetCloud } from "./deploy-with-rivet-cloud"; +import { deployWithRivetSelfHosted } from "./deploy-with-rivet-self-hosted"; +import type { DeployRequest, LogCallback } from "./utils"; + +const app = new Hono(); + +app.use(logger()); + +app.post("/api/deploy", async (c) => { + const body = await c.req.json(); + + return streamSSE(c, async (stream) => { + const log: LogCallback = async (message: string) => { + await stream.writeSSE({ data: message, event: "log" }); + }; + + try { + const isCloud = "cloud" in body.kind; + + let result; + if (isCloud) { + result = await deployWithRivetCloud(body, log); + } else { + result = await deployWithRivetSelfHosted(body, log); + } + + await stream.writeSSE({ + data: JSON.stringify(result), + event: "result", + }); + } catch (error) { + console.error("Deployment error:", error); + const errorMessage = error instanceof Error ? error.message : String(error); + + await stream.writeSSE({ + data: errorMessage, + event: "error", + }); + } + }); +}); + +// Global error handlers +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection at:', promise, 'reason:', reason); +}); + +process.on('uncaughtException', (error) => { + console.error('Uncaught Exception:', error); + process.exit(1); +}); + +const PORT = Number(process.env.PORT) || 3001; +serve({ + fetch: app.fetch, + port: PORT, +}); diff --git a/examples/ai-and-user-generated-actors-freestyle/src/backend/utils.ts b/examples/ai-and-user-generated-actors-freestyle/src/backend/utils.ts new file mode 100644 index 0000000000..3c841b12c6 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/src/backend/utils.ts @@ -0,0 +1,142 @@ +import { RivetClient } from "@rivetkit/engine-api-full"; +import { FreestyleSandboxes } from "freestyle-sandboxes"; +import { prepareDirForDeploymentSync } from "freestyle-sandboxes/utils"; +import { writeFile, cp } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { $ } from "execa"; + +export type LogCallback = (message: string) => Promise; + +export interface CloudDeployRequest { + cloudEndpoint: string; + cloudToken: string; + engineEndpoint: string; +} + +export interface SelfHostedDeployRequest { + endpoint: string; + token: string; +} + +export interface DeployRequest { + registryCode: string; + appCode: string; + datacenter?: string; + freestyleDomain: string; + freestyleApiKey: string; + kind: + | { cloud: CloudDeployRequest } + | { selfHosted: SelfHostedDeployRequest }; +} + +/** Assemble the repository that we're going to deploy to Freestyle. */ +export async function setupRepo(config: { + registryCode: string; + appCode: string; + log: LogCallback; +}): Promise { + await config.log("Preparing project files"); + const tmpDir = join(tmpdir(), `rivet-deploy-${Date.now()}`); + const templateDir = join(process.cwd(), "template"); + + await cp(templateDir, tmpDir, { recursive: true }); + + await writeFile(join(tmpDir, "src/backend/registry.ts"), config.registryCode); + await writeFile(join(tmpDir, "src/frontend/App.tsx"), config.appCode); + + return tmpDir; +} + +/** Build frontend with Vite. */ +export async function buildFrontend( + projectDir: string, + envVars: Record, + log: LogCallback, +) { + await log("Installing dependencies"); + await $({ cwd: projectDir })`pnpm install --no-frozen-lockfile`; + + await log("Building frontend"); + await $({ cwd: projectDir, env: { ...process.env, ...envVars } })`pnpm run build:frontend`; +} + +/** + * Deploy the application to Freestyle Sandboxes. + * + * Sets up the project directory and deploys the backend to Freestyle. + */ +export async function deployToFreestyle(config: { + registryCode: string; + appCode: string; + domain: string; + apiKey: string; + envVars: Record; + log: LogCallback; +}): Promise<{ deploymentId: string }> { + const projectDir = await setupRepo({ + registryCode: config.registryCode, + appCode: config.appCode, + log: config.log, + }); + + await buildFrontend(projectDir, config.envVars, config.log); + + await config.log("Deploying to Freestyle"); + + const freestyle = new FreestyleSandboxes({ + apiKey: config.apiKey, + }); + + const deploymentSource = prepareDirForDeploymentSync(projectDir); + + const result = await freestyle.deployWeb(deploymentSource, { + envVars: { + LOG_LEVEL: "debug", + FREESTYLE_ENDPOINT: `https://${config.domain}`, + RIVET_RUNNER_KIND: "serverless", + ...config.envVars, + }, + timeout: 60 * 5, + entrypoint: "src/backend/server.ts", + domains: [config.domain], + build: false, + }); + + return { deploymentId: result.deploymentId }; +} + +/** + * Configure a serverless runner in Rivet for a specific datacenter. + */ +export async function configureRivetServerless(config: { + rivet: RivetClient; + domain: string; + namespace: string; + datacenter?: string; + log: LogCallback; +}) { + const datacenter = config.datacenter || "us-west-1"; + await config.log(`Configuring runner in ${datacenter}`); + + await config.rivet.runnerConfigsUpsert("default", { + datacenters: { + [datacenter]: { + serverless: { + url: `https://${config.domain}/api/rivet`, + headers: {}, + runnersMargin: 0, + minRunners: 0, + maxRunners: 1_000, + slotsPerRunner: 1, + requestLifespan: 60 * 5, + }, + }, + }, + namespace: config.namespace, + }); +} + +export function generateNamespaceName(): string { + return `ns-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; +} diff --git a/examples/ai-and-user-generated-actors-freestyle/src/frontend/App.css b/examples/ai-and-user-generated-actors-freestyle/src/frontend/App.css new file mode 100644 index 0000000000..d8f382a497 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/src/frontend/App.css @@ -0,0 +1,280 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif; + margin: 0; + padding: 0; + background-color: rgb(10, 10, 10); + overflow: hidden; +} + +.app { + height: 100vh; + display: flex; + flex-direction: column; + background: rgb(10, 10, 10); +} + +.ide-layout { + display: flex; + height: 100vh; + overflow: hidden; + background: rgb(10, 10, 10); +} + +.editors-row { + flex: 1; + display: flex; + overflow: hidden; +} + +.editor-section { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + border-right: 1px solid #2c2c2e; +} + +.editor-section:last-child { + border-right: none; +} + +.editor-header { + background: rgb(10, 10, 10); + color: #ffffff; + padding: 12px 20px; + font-weight: 500; + font-size: 0.95em; + border-bottom: 1px solid #2c2c2e; +} + +.editor-section textarea { + flex: 1; + overflow: auto !important; +} + +.deploy-panel { + width: 400px; + background: rgb(10, 10, 10); + border-left: 1px solid #2c2c2e; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.panel-header { + background: rgb(10, 10, 10); + color: #ffffff; + padding: 12px 20px; + text-align: left; + border-bottom: 1px solid #2c2c2e; +} + +.panel-header h2 { + margin: 0; + font-size: 0.95em; + font-weight: 500; +} + +.tabs { + display: flex; + border-bottom: 1px solid #2c2c2e; + background: rgb(10, 10, 10); +} + +.tabs button { + flex: 1; + padding: 12px 16px; + background: rgb(10, 10, 10); + border: none; + color: #8e8e93; + cursor: pointer; + font-size: 0.9em; + border-bottom: 3px solid transparent; + transition: all 0.2s; +} + +.tabs button:hover { + background: #2c2c2e; + color: #ffffff; +} + +.tabs button.active { + color: rgb(255, 79, 0); + border-bottom-color: rgb(255, 79, 0); +} + +.panel-content { + flex: 1; + overflow-y: auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 20px; + background: rgb(10, 10, 10); +} + +.env-vars-section { + flex-shrink: 0; +} + +.env-vars-section h3 { + margin-bottom: 15px; + color: #ffffff; + font-size: 1em; + font-weight: 600; +} + +.env-vars { + display: flex; + flex-direction: column; + gap: 12px; +} + +.env-var { + display: flex; + flex-direction: column; + gap: 6px; +} + +.env-var label { + color: #8e8e93; + font-size: 0.85em; + font-weight: 500; +} + +.env-var input { + padding: 8px 12px; + border: 1px solid #3a3a3c; + border-radius: 8px; + font-size: 0.9em; + transition: all 0.2s; + background: #2c2c2e; + color: #ffffff; +} + +.env-var input::placeholder { + color: #8e8e93; +} + +.env-var input:focus { + outline: none; + border-color: rgb(255, 79, 0); + box-shadow: 0 0 0 3px rgba(255, 79, 0, 0.1); +} + +.deploy-section { + flex-shrink: 0; +} + +.deploy-button { + width: 100%; + padding: 12px 24px; + background: white; + color: black; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 1em; + font-weight: 500; + transition: all 0.2s; +} + +.deploy-button:hover:not(:disabled) { + background: #e0e0e0; +} + +.deploy-button:disabled { + background: #3a3a3c; + color: #8e8e93; + cursor: not-allowed; +} + +.log-section { + flex-shrink: 0; + background: transparent; + border: 1px solid #2c2c2e; + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 8px; + max-height: 300px; + overflow-y: auto; +} + +.log-section h3 { + color: #ffffff; + font-size: 1em; + font-weight: 600; + margin: 0; +} + +.log-section > div { + font-family: ui-monospace, SFMono-Regular, SF Mono, Consolas, Liberation Mono, Menlo, monospace; + font-size: 0.8em; + color: #8e8e93; + line-height: 1.5; +} + +.log-section.deploying > div:last-child { + color: rgb(255, 79, 0); + font-weight: 500; +} + +.deployment-success { + background: transparent; + border: 1px solid #2c2c2e; + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.deployment-success h3 { + color: #ffffff; + font-size: 1em; + font-weight: 600; + margin: 0; +} + +.deployment-success a { + color: rgb(255, 79, 0); + text-decoration: none; + font-size: 0.9em; +} + +.deployment-success a:hover { + text-decoration: underline; +} + +/* Code editor styling */ +textarea { + outline: none !important; + border: none !important; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: rgb(10, 10, 10); +} + +::-webkit-scrollbar-thumb { + background: #3a3a3c; + border-radius: 5px; +} + +::-webkit-scrollbar-thumb:hover { + background: #48484a; +} diff --git a/examples/ai-and-user-generated-actors-freestyle/src/frontend/App.tsx b/examples/ai-and-user-generated-actors-freestyle/src/frontend/App.tsx new file mode 100644 index 0000000000..84f3100de3 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/src/frontend/App.tsx @@ -0,0 +1,319 @@ +import { useState } from "react"; +import Editor, { type OnMount } from "@monaco-editor/react"; +import "./App.css"; +import DEFAULT_REGISTRY from "../../template/src/backend/registry.ts?raw"; +import DEFAULT_APP from "../../template/src/frontend/App.tsx?raw"; +import { DeployRequest } from "../backend/utils"; + +type DeploymentTarget = "cloud" | "selfHosted"; + +interface DeployConfig { + target: DeploymentTarget; + freestyleDomain: string; + freestyleApiKey: string; + // Cloud-specific + cloudApiUrl: string; + cloudApiToken: string; + cloudEngineEndpoint: string; + // Self-hosted specific + selfHostedEndpoint: string; + selfHostedToken: string; +} + +export function App() { + const [registryCode, setRegistryCode] = useState(DEFAULT_REGISTRY); + const [appCode, setAppCode] = useState(DEFAULT_APP); + const [deploying, setDeploying] = useState(false); + const [deploymentLog, setDeploymentLog] = useState([]); + const [deploymentUrl, setDeploymentUrl] = useState(null); + const [dashboardUrl, setDashboardUrl] = useState(null); + const [freestyleUrl, setFreestyleUrl] = useState(null); + + const handleEditorMount: OnMount = (editor, monaco) => { + // Disable TypeScript diagnostics since template code isn't valid standalone + monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ + noSemanticValidation: true, + noSyntaxValidation: true, + }); + }; + + const [config, setConfig] = useState({ + target: "cloud", + freestyleDomain: "", + freestyleApiKey: "", + cloudApiUrl: import.meta.env.VITE_RIVET_CLOUD_ENDPOINT || "https://api-cloud.rivet.dev", + cloudApiToken: "", + cloudEngineEndpoint: import.meta.env.VITE_RIVET_ENGINE_ENDPOINT || "https://api.rivet.dev", + selfHostedEndpoint: "", + selfHostedToken: "", + }); + + const addLog = (message: string) => { + setDeploymentLog((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]); + }; + + const handleDeploy = async () => { + setDeploying(true); + setDeploymentLog([]); + setDeploymentUrl(null); + setDashboardUrl(null); + setFreestyleUrl(null); + + try { + const datacenter = import.meta.env.VITE_RIVET_DATACENTER || "us-west-1"; + + // Build request payload + const payload: DeployRequest = { + registryCode, + appCode, + datacenter, + freestyleDomain: config.freestyleDomain, + freestyleApiKey: config.freestyleApiKey, + kind: config.target === "cloud" + ? { + cloud: { + cloudEndpoint: config.cloudApiUrl, + cloudToken: config.cloudApiToken, + engineEndpoint: config.cloudEngineEndpoint, + } + } + : { + selfHosted: { + endpoint: config.selfHostedEndpoint, + token: config.selfHostedToken, + } + } + }; + + const response = await fetch("/api/deploy", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error("Deployment request failed"); + } + + // Read SSE stream + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("No response body"); + } + + const decoder = new TextDecoder(); + let buffer = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + let currentEvent = ""; + for (const line of lines) { + if (line.startsWith("event: ")) { + currentEvent = line.slice(7); + } else if (line.startsWith("data: ")) { + const data = line.slice(6); + if (currentEvent === "log") { + addLog(data); + } else if (currentEvent === "result") { + const result = JSON.parse(data); + setDeploymentUrl(`https://${config.freestyleDomain}/`); + if (result.dashboardUrl) { + setDashboardUrl(result.dashboardUrl); + } + if (result.freestyleUrl) { + setFreestyleUrl(result.freestyleUrl); + } + } else if (currentEvent === "error") { + addLog(`Error: ${data}`); + } + currentEvent = ""; + } + } + } + } catch (error) { + addLog(`Error: ${error instanceof Error ? error.message : String(error)}`); + } finally { + setDeploying(false); + } + }; + + return ( +
+
+
+
+
src/backend/registry.ts
+ setRegistryCode(value || "")} + onMount={handleEditorMount} + theme="vs-dark" + options={{ + minimap: { enabled: false }, + fontSize: 14, + scrollBeyondLastLine: false, + automaticLayout: true, + }} + /> +
+ +
+
src/frontend/App.tsx
+ setAppCode(value || "")} + onMount={handleEditorMount} + theme="vs-dark" + options={{ + minimap: { enabled: false }, + fontSize: 14, + scrollBeyondLastLine: false, + automaticLayout: true, + }} + /> +
+
+ +
+
+

Deploy to Freestyle

+
+ +
+ + +
+ +
+
+

Configuration

+
+ {config.target === "cloud" && ( +
+ + + setConfig({ ...config, cloudApiToken: e.target.value }) + } + placeholder="Required" + /> +
+ )} + {config.target === "selfHosted" && ( + <> +
+ + + setConfig({ ...config, selfHostedEndpoint: e.target.value }) + } + placeholder="Required" + /> +
+
+ + + setConfig({ ...config, selfHostedToken: e.target.value }) + } + placeholder="Required" + /> +
+ + )} +
+ + + setConfig({ ...config, freestyleDomain: e.target.value }) + } + placeholder="myapp.style.dev" + /> +
+
+ + + setConfig({ ...config, freestyleApiKey: e.target.value }) + } + placeholder="Required" + /> +
+
+
+ +
+ +
+ + {deploymentLog.length > 0 && ( +
+

Deployment Log

+ {deploymentLog.map((log, i) => ( +
{log}
+ ))} +
+ )} + + {deploymentUrl && ( +
+

Deployment Complete

+ + Open App › + + {dashboardUrl && ( + + Rivet Namespace › + + )} + {freestyleUrl && ( + + Freestyle Deployment › + + )} +
+ )} +
+
+
+
+ ); +} diff --git a/examples/ai-and-user-generated-actors-freestyle/src/frontend/index.html b/examples/ai-and-user-generated-actors-freestyle/src/frontend/index.html new file mode 100644 index 0000000000..d1c42a1b9c --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/src/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + User and AI Generated Rivet Backend + + +
+ + + diff --git a/examples/ai-and-user-generated-actors-freestyle/src/frontend/main.tsx b/examples/ai-and-user-generated-actors-freestyle/src/frontend/main.tsx new file mode 100644 index 0000000000..1961235a6b --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/src/frontend/main.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { App } from "./App"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/examples/ai-and-user-generated-actors-freestyle/template/package.json b/examples/ai-and-user-generated-actors-freestyle/template/package.json new file mode 100644 index 0000000000..4a40ca8515 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/template/package.json @@ -0,0 +1,27 @@ +{ + "name": "deployed-freestyle-app", + "version": "1.0.0", + "private": true, + "type": "module", + "packageManager": "pnpm@10.15.0", + "scripts": { + "dev:backend": "deno run --watch --allow-all src/backend/server.ts", + "dev:frontend": "vite", + "build:frontend": "vite build" + }, + "dependencies": { + "@rivetkit/react": "^2.0.24", + "hono": "4.9.8", + "on-change": "^5.0.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "rivetkit": "2.0.24" + }, + "devDependencies": { + "@types/react": "^19.0.9", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.3", + "vite": "^6.0.11" + } +} diff --git a/examples/ai-and-user-generated-actors-freestyle/template/src/backend/registry.ts b/examples/ai-and-user-generated-actors-freestyle/template/src/backend/registry.ts new file mode 100644 index 0000000000..41dfcd357a --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/template/src/backend/registry.ts @@ -0,0 +1,27 @@ +import { actor, setup } from "rivetkit"; + +export const counter = actor({ + state: { + count: 0, + }, + + actions: { + increment: (c) => { + c.state.count++; + c.broadcast("countChanged", c.state.count); + return c.state.count; + }, + + decrement: (c) => { + c.state.count--; + c.broadcast("countChanged", c.state.count); + return c.state.count; + }, + + getCount: (c) => c.state.count, + }, +}); + +export const registry = setup({ + use: { counter }, +}); diff --git a/examples/ai-and-user-generated-actors-freestyle/template/src/backend/server.ts b/examples/ai-and-user-generated-actors-freestyle/template/src/backend/server.ts new file mode 100644 index 0000000000..7bb59c70b1 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/template/src/backend/server.ts @@ -0,0 +1,33 @@ +import { Hono } from "hono"; +import { upgradeWebSocket } from "hono/deno"; +import { serveStatic } from "hono/deno"; +import { registry } from "./registry.ts"; + +globalThis.addEventListener("unhandledrejection", (event) => { + console.error("Unhandled promise rejection:", event.reason); + event.preventDefault(); +}); + +const serverOutput = registry.start({ + inspector: { + enabled: true, + }, + runnerKind: "serverless", + disableDefaultServer: true, + noWelcome: true, + runEngine: false, + autoConfigureServerless: false, + basePath: "/api/rivet", + getUpgradeWebSocket: () => upgradeWebSocket, +}); + +const app = new Hono(); + +app.use("/api/rivet/*", async (c) => { + return await serverOutput.fetch(c.req.raw); +}); + +app.use("*", serveStatic({ root: "./public" })); + +// @ts-expect-error +Deno.serve(app.fetch); diff --git a/examples/ai-and-user-generated-actors-freestyle/template/src/frontend/App.tsx b/examples/ai-and-user-generated-actors-freestyle/template/src/frontend/App.tsx new file mode 100644 index 0000000000..446cfeb559 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/template/src/frontend/App.tsx @@ -0,0 +1,71 @@ +import { createRivetKit } from "@rivetkit/react"; +import { useEffect, useState } from "react"; +import type { registry } from "../backend/registry"; + +console.log("Environment variables:", { + VITE_RIVET_ENDPOINT: import.meta.env.VITE_RIVET_ENDPOINT, + VITE_RIVET_NAMESPACE: import.meta.env.VITE_RIVET_NAMESPACE, + VITE_RIVET_TOKEN: import.meta.env.VITE_RIVET_TOKEN, +}); + +const { useActor } = createRivetKit({ + endpoint: import.meta.env.VITE_RIVET_ENDPOINT, + namespace: import.meta.env.VITE_RIVET_NAMESPACE, + token: import.meta.env.VITE_RIVET_TOKEN, +}); + +export function App() { + const [count, setCount] = useState(0); + + const counter = useActor({ + name: "counter", + key: ["global"], + }); + + useEffect(() => { + if (counter.connection) { + counter.connection.getCount().then(setCount); + } + }, [counter.connection]); + + counter.useEvent("countChanged", (newCount: number) => { + setCount(newCount); + }); + + const increment = async () => { + if (counter.connection) { + const newCount = await counter.connection.increment(); + setCount(newCount); + } + }; + + const decrement = async () => { + if (counter.connection) { + const newCount = await counter.connection.decrement(); + setCount(newCount); + } + }; + + return ( +
+

Realtime Counter

+
+ Try opening another tab +
+
+

{count}

+
+
+ + +
+
+ {counter.connection ? "Connected" : "Connecting..."} +
+
+ ); +} diff --git a/examples/ai-and-user-generated-actors-freestyle/template/src/frontend/index.html b/examples/ai-and-user-generated-actors-freestyle/template/src/frontend/index.html new file mode 100644 index 0000000000..03772d4191 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/template/src/frontend/index.html @@ -0,0 +1,95 @@ + + + + + + Rivet Counter + + + +
+ + + diff --git a/examples/freestyle/src/frontend/main.tsx b/examples/ai-and-user-generated-actors-freestyle/template/src/frontend/main.tsx similarity index 100% rename from examples/freestyle/src/frontend/main.tsx rename to examples/ai-and-user-generated-actors-freestyle/template/src/frontend/main.tsx diff --git a/examples/ai-and-user-generated-actors-freestyle/template/tsconfig.json b/examples/ai-and-user-generated-actors-freestyle/template/tsconfig.json new file mode 100644 index 0000000000..bf825cc1b4 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/template/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["esnext", "dom"], + "jsx": "react-jsx", + "module": "esnext", + "moduleResolution": "bundler", + "types": ["node", "vite/client"], + "resolveJsonModule": true, + "allowJs": true, + "checkJs": false, + "noEmit": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/examples/freestyle/vite.config.ts b/examples/ai-and-user-generated-actors-freestyle/template/vite.config.ts similarity index 83% rename from examples/freestyle/vite.config.ts rename to examples/ai-and-user-generated-actors-freestyle/template/vite.config.ts index 6495f1f870..ac1e033460 100644 --- a/examples/freestyle/vite.config.ts +++ b/examples/ai-and-user-generated-actors-freestyle/template/vite.config.ts @@ -5,9 +5,8 @@ export default defineConfig({ plugins: [react()], root: "src/frontend", build: { - outDir: "../../dist/public", + outDir: "../../public", }, - envDir: ".", server: { host: "0.0.0.0", port: 5173, diff --git a/examples/ai-and-user-generated-actors-freestyle/tests/deploy.test.ts b/examples/ai-and-user-generated-actors-freestyle/tests/deploy.test.ts new file mode 100644 index 0000000000..8758ed4d11 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/tests/deploy.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { deployWithRivetCloud } from "../src/backend/deploy-with-rivet-cloud"; +import { deployWithRivetSelfHosted } from "../src/backend/deploy-with-rivet-self-hosted"; +import type { DeployRequest, LogCallback } from "../src/backend/utils"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +// Simple log callback for tests +const testLog: LogCallback = async (message: string) => { + console.log(message); +}; + +// Load sample code from template files +const SAMPLE_REGISTRY_CODE = readFileSync( + join(process.cwd(), "template/src/backend/registry.ts"), + "utf-8", +); + +const SAMPLE_APP_CODE = readFileSync( + join(process.cwd(), "template/src/frontend/App.tsx"), + "utf-8", +); + +describe("Deploy Functions", () => { + // Load environment variables + beforeAll(() => { + // Check if required environment variables are set + const requiredEnvVars = ["FREESTYLE_DOMAIN", "FREESTYLE_API_KEY"]; + + for (const envVar of requiredEnvVars) { + if (!process.env[envVar]) { + throw new Error( + `Missing required environment variable: ${envVar}`, + ); + } + } + }); + + describe("deployWithRivetCloud", () => { + it.skip("should deploy successfully with Rivet Cloud", async () => { + // Skip this test if cloud credentials are not provided + if ( + !process.env.RIVET_CLOUD_ENDPOINT || + !process.env.RIVET_CLOUD_TOKEN || + !process.env.RIVET_ENGINE_ENDPOINT + ) { + console.log( + "Skipping cloud deployment test - missing credentials", + ); + return; + } + + const request: DeployRequest = { + registryCode: SAMPLE_REGISTRY_CODE, + appCode: SAMPLE_APP_CODE, + datacenter: process.env.RIVET_DATACENTER, + freestyleDomain: process.env.FREESTYLE_DOMAIN!, + freestyleApiKey: process.env.FREESTYLE_API_KEY!, + kind: { + cloud: { + cloudEndpoint: process.env.RIVET_CLOUD_ENDPOINT, + cloudToken: process.env.RIVET_CLOUD_TOKEN, + engineEndpoint: process.env.RIVET_ENGINE_ENDPOINT, + }, + }, + }; + + const result = await deployWithRivetCloud(request, testLog); + + expect(result).toBeDefined(); + expect(result.success).toBe(true); + expect(result.tokens).toBeDefined(); + expect(result.tokens.runnerToken).toBeDefined(); + expect(result.tokens.publishableToken).toBeDefined(); + }, 300000); // 5 minute timeout for deployment + }); + + describe("deployWithRivetSelfHosted", () => { + it.skip("should deploy successfully with Rivet Self-Hosted", async () => { + // Skip this test if self-hosted credentials are not provided + if (!process.env.RIVET_ENDPOINT || !process.env.RIVET_TOKEN) { + console.log( + "Skipping self-hosted deployment test - missing credentials", + ); + return; + } + + const request: DeployRequest = { + registryCode: SAMPLE_REGISTRY_CODE, + appCode: SAMPLE_APP_CODE, + datacenter: process.env.RIVET_DATACENTER, + freestyleDomain: process.env.FREESTYLE_DOMAIN!, + freestyleApiKey: process.env.FREESTYLE_API_KEY!, + kind: { + selfHosted: { + endpoint: process.env.RIVET_ENDPOINT, + token: process.env.RIVET_TOKEN, + }, + }, + }; + + const result = await deployWithRivetSelfHosted(request, testLog); + + expect(result).toBeDefined(); + expect(result.success).toBe(true); + }, 300000); // 5 minute timeout for deployment + }); +}); diff --git a/examples/ai-and-user-generated-actors-freestyle/tsconfig.json b/examples/ai-and-user-generated-actors-freestyle/tsconfig.json new file mode 100644 index 0000000000..b7e2f539fc --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/ai-and-user-generated-actors-freestyle/tsconfig.node.json b/examples/ai-and-user-generated-actors-freestyle/tsconfig.node.json new file mode 100644 index 0000000000..f4e3be23a5 --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts", "server"] +} diff --git a/examples/ai-and-user-generated-actors-freestyle/vite.config.ts b/examples/ai-and-user-generated-actors-freestyle/vite.config.ts new file mode 100644 index 0000000000..75cbbebbaa --- /dev/null +++ b/examples/ai-and-user-generated-actors-freestyle/vite.config.ts @@ -0,0 +1,20 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + root: "src/frontend", + build: { + emptyOutDir: true, + }, + server: { + host: "0.0.0.0", + port: 5173, + proxy: { + "/api": { + target: "http://localhost:3001", + changeOrigin: true, + }, + }, + }, +}); diff --git a/examples/freestyle/vitest.config.ts b/examples/ai-and-user-generated-actors-freestyle/vitest.config.ts similarity index 52% rename from examples/freestyle/vitest.config.ts rename to examples/ai-and-user-generated-actors-freestyle/vitest.config.ts index 5bdee00206..e4f8a9c641 100644 --- a/examples/freestyle/vitest.config.ts +++ b/examples/ai-and-user-generated-actors-freestyle/vitest.config.ts @@ -2,6 +2,9 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - include: ["tests/**/*.test.ts"], + globals: true, + environment: "node", + testTimeout: 120000, + hookTimeout: 120000, }, }); diff --git a/examples/freestyle/.env.sample b/examples/freestyle/.env.sample deleted file mode 100644 index 16f9f1cfe1..0000000000 --- a/examples/freestyle/.env.sample +++ /dev/null @@ -1,14 +0,0 @@ -FREESTYLE_API_KEY="" -FREESTYLE_DOMAIN="example.style.dev" -RIVET_TOKEN="" - -# To enable serverless -RIVET_ENDPOINT="" -RIVET_RUNNER_KIND="serverless" - -# For accessing actor (not relevant if using self-hosted Rivet Engine) -RIVET_PUBLISHABLE_TOKEN="" # default: "" - -# Optional variables with defaults: -RIVET_NAMESPACE="default" # default: "default" -RIVET_RUNNER_NAME="freestyle-runner" # default: "freestyle-runner" \ No newline at end of file diff --git a/examples/freestyle/.gitignore b/examples/freestyle/.gitignore deleted file mode 100644 index 79b7a1192f..0000000000 --- a/examples/freestyle/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.actorcore -node_modules \ No newline at end of file diff --git a/examples/freestyle/README.md b/examples/freestyle/README.md deleted file mode 100644 index 83e9a2b4ce..0000000000 --- a/examples/freestyle/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# Freestyle Deployment for RivetKit - -Example project demonstrating serverless deployment of RivetKit actors to [Freestyle](https://freestyle.sh) with [RivetKit](https://rivetkit.org). - -[Learn More →](https://github.com/rivet-dev/rivetkit) - -[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues) - - -## What is this? - -Freestyle is unique from other providers since it is built to deploy untrusted AI-generated or user-generated code. This enables your application to deploy vibe-coded or user-provided backends on Rivet and Freestyle. This example showcases a real-time stateful chat app that can be deployed to FreeStyle's [Web Deployment](https://docs.freestyle.sh/web/overview) platform. - -## Getting Started - -### Prerequisites - -- Node.js 18+ -- Deno (for development) - -**Note**: Deno is required since Freestyle uses Deno for their Web Deployments under the hood - -### Installation - -```sh -git clone https://github.com/rivet-dev/rivetkit -cd rivetkit/examples/freestyle -pnpm install -``` - -### Development - -```sh -pnpm run dev -``` - -Open your browser to `http://localhost:5173` to see the application. - -```sh -RIVET_RUNNER_KIND=serverless VITE_RIVET_ENDPOINT="$RIVET_ENDPOINT" pnpm run dev -``` - -### Deploy to Freestyle - -```sh -# Set env vars -export FREESTYLE_DOMAIN="my-domain.style.dev" # Set this to any unique *.style.dev domain -export FREESTYLE_API_KEY="XXXX" # See https://admin.freestyle.sh/dashboard/api-tokens -export RIVET_ENDPOINT="http://api.rivet.gg" -export RIVET_NAMESPACE="XXXX" # Creates new namespace if does not exist -export RIVET_TOKEN="XXXX" # Rivet Service token -export RIVET_PUBLISHABLE_TOKEN="XXXX" # For connecting to Rivet Actors - -pnpm run freestyle:deploy -``` - -Open your browser to your Freestyle domain to see your application connect to Rivet deployed on Freestyle. - -If self-hosting Rivet: -1. **Important**: `RIVET_ENDPOINT` must be public to the internet. -2. `RIVET_PUBLISHABLE_TOKEN` can be kept empty. - -## License - -Apache 2.0 diff --git a/examples/freestyle/deno.json b/examples/freestyle/deno.json deleted file mode 100644 index 514de9dd79..0000000000 --- a/examples/freestyle/deno.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "imports": { - "os": "node:os", - "path": "node:path", - "fs": "node:fs", - "fs/promises": "node:fs/promises", - "crypto": "node:crypto", - "rivetkit": "npm:rivetkit", - "hono": "npm:hono" - } -} diff --git a/examples/freestyle/scripts/freestyle-deploy.ts b/examples/freestyle/scripts/freestyle-deploy.ts deleted file mode 100644 index 5aa230cb15..0000000000 --- a/examples/freestyle/scripts/freestyle-deploy.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { execSync } from "node:child_process"; -import { readFileSync } from "node:fs"; -import { type Rivet, RivetClient } from "@rivetkit/engine-api-full"; -import dotenv from "dotenv"; -import { FreestyleSandboxes } from "freestyle-sandboxes"; -import { prepareDirForDeploymentSync } from "freestyle-sandboxes/utils"; - -dotenv.config({ path: new URL("../.env", import.meta.url).pathname }); - -const FREESTYLE_DOMAIN = getEnv("FREESTYLE_DOMAIN"); -const FREESTYLE_API_KEY = getEnv("FREESTYLE_API_KEY"); -const RIVET_ENDPOINT = getEnv("RIVET_ENDPOINT"); -const RIVET_TOKEN = getEnv("RIVET_TOKEN"); -const RIVET_NAMESPACE_NAME = getEnv("RIVET_NAMESPACE"); -const RIVET_RUNNER_NAME = "freestyle"; - -function getEnv(key: string): string { - const value = process.env[key]; - if (typeof value === "undefined") { - throw new Error(`Missing env var: ${key}`); - } - return value; -} - -const rivet = new RivetClient({ - environment: RIVET_ENDPOINT, - token: RIVET_TOKEN, -}); - -const freestyle = new FreestyleSandboxes({ - apiKey: FREESTYLE_API_KEY, -}); - -async function main() { - const namespace = await getOrCreateNamespace({ - displayName: RIVET_NAMESPACE_NAME, - name: RIVET_NAMESPACE_NAME, - }); - console.log("Got namespace " + namespace.name); - - runBuildSteps(); - - await deployToFreestyle(); - console.log("Deployed to freestyle"); - - await updateRunnerConfig(namespace); - console.log("Updated runner config"); - - console.log("🎉 Deployment complete! 🎉"); - console.log( - "Visit https://" + - FREESTYLE_DOMAIN + - "/ to see your frontend, which is connected to Rivet", - ); -} - -function runBuildSteps() { - console.log("Running build steps..."); - - console.log("- Running vite build"); - execSync("vite build", { - stdio: "inherit", - env: { - ...process.env, - VITE_RIVET_ENDPOINT: RIVET_ENDPOINT, - VITE_RIVET_NAMESPACE: RIVET_NAMESPACE_NAME, - VITE_RIVET_RUNNER_NAME: RIVET_RUNNER_NAME, - }, - }); - - console.log("- Running tsup"); - execSync("tsup", { - stdio: "inherit", - }); - - console.log("Build complete!"); -} - -async function getOrCreateNamespace({ - name, - displayName, -}: { - name: string; - displayName: string; -}): Promise { - console.log("- Checking for existing " + name + " namespace"); - const { namespaces } = await rivet.namespaces.list({ - limit: 32, - }); - const existing = namespaces.find((ns) => ns.name === name); - if (existing) { - console.log("- Found existing namespace " + name); - return existing; - } - console.log("- Creating namespace " + name); - const { namespace } = await rivet.namespaces.create({ - displayName, - name, - }); - return namespace; -} - -async function updateRunnerConfig(namespace: Rivet.Namespace) { - console.log("- Updating runner config for " + RIVET_RUNNER_NAME); - await rivet.runnerConfigs.upsert(RIVET_RUNNER_NAME, { - serverless: { - url: "https://" + FREESTYLE_DOMAIN + "/api/start", - headers: {}, - runnersMargin: 1, - minRunners: 1, - maxRunners: 1, - slotsPerRunner: 1, - requestLifespan: 60 * 4 + 30, - }, - namespace: namespace.name, - }); -} - -async function deployToFreestyle() { - console.log("- Deploying to freestyle at https://" + FREESTYLE_DOMAIN); - - const buildDir = prepareDirForDeploymentSync( - new URL("../dist", import.meta.url).pathname, - ); - if (buildDir.kind === "files") { - buildDir.files["deno.json"] = { - // Fix imports for Deno - content: readFileSync( - new URL("../deno.json", import.meta.url).pathname, - "utf-8", - ), - encoding: "utf-8", - }; - } else { - throw new Error("Expected buildDir to be files"); - } - const res = await freestyle.deployWeb(buildDir, { - envVars: { - LOG_LEVEL: "debug", - FREESTYLE_ENDPOINT: `https://${FREESTYLE_DOMAIN}`, - RIVET_ENDPOINT, - RIVET_RUNNER_KIND: "serverless", - }, - timeout: 60 * 5, - entrypoint: "server.cjs", - domains: [FREESTYLE_DOMAIN], - build: false, - }); - - console.log("Deployment id=" + res.deploymentId); -} - -main(); diff --git a/examples/freestyle/src/backend/registry.ts b/examples/freestyle/src/backend/registry.ts deleted file mode 100644 index 34e333eb1a..0000000000 --- a/examples/freestyle/src/backend/registry.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { actor, setup } from "rivetkit"; - -export type Message = { sender: string; text: string; timestamp: number }; - -export const chatRoom = actor({ - // Persistent state that survives restarts: https://rivet.dev/docs/actors/state - state: { - messages: [] as Message[], - }, - - actions: { - // Callable functions from clients: https://rivet.dev/docs/actors/actions - sendMessage: (c, sender: string, text: string) => { - const message = { sender, text, timestamp: Date.now() }; - // State changes are automatically persisted - c.state.messages.push(message); - // Send events to all connected clients: https://rivet.dev/docs/actors/events - c.broadcast("newMessage", message); - return message; - }, - - getHistory: (c) => c.state.messages, - }, -}); - -// Register actors for use: https://rivet.dev/docs/setup -export const registry = setup({ - use: { chatRoom }, -}); diff --git a/examples/freestyle/src/backend/server.ts b/examples/freestyle/src/backend/server.ts deleted file mode 100644 index 65940e5ec7..0000000000 --- a/examples/freestyle/src/backend/server.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Hono } from "hono"; -import { serveStatic, upgradeWebSocket } from "hono/deno"; -import { registry } from "./registry"; - -const serverOutput = registry.start({ - inspector: { - enabled: true, - }, - disableDefaultServer: true, - basePath: "/api", - getUpgradeWebSocket: () => upgradeWebSocket, - overrideServerAddress: `${process.env.FREESTYLE_ENDPOINT ?? "http://localhost:8080"}/api`, -}); - -const app = new Hono(); -app.use("/api/*", async (c) => { - return await serverOutput.fetch(c.req.raw); -}); -app.use("*", serveStatic({ root: "./public" })); - -// Under the hood, Freestyle uses Deno -// for their Web Deploy instances -// @ts-expect-error -Deno.serve({ port: 8080 }, app.fetch); diff --git a/examples/freestyle/src/frontend/App.tsx b/examples/freestyle/src/frontend/App.tsx deleted file mode 100644 index f8d8e17b6a..0000000000 --- a/examples/freestyle/src/frontend/App.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { createRivetKit } from "@rivetkit/react"; -import { useEffect, useState } from "react"; -import type { Message, registry } from "../backend/registry"; - -const { useActor } = createRivetKit({ - endpoint: import.meta.env.VITE_RIVET_ENDPOINT ?? "http://localhost:8080/api", - namespace: import.meta.env.VITE_RIVET_NAMESPACE, - runnerName: import.meta.env.VITE_RIVET_RUNNER_NAME ?? "freestyle-runner", -}); - -export function App() { - const [roomId, setRoomId] = useState("general"); - const [username, setUsername] = useState("User"); - const [input, setInput] = useState(""); - const [messages, setMessages] = useState([]); - - const chatRoom = useActor({ - name: "chatRoom", - key: [roomId], - }); - - useEffect(() => { - if (chatRoom.connection) { - chatRoom.connection.getHistory().then(setMessages); - } - }, [chatRoom.connection]); - - chatRoom.useEvent("newMessage", (message: Message) => { - setMessages((prev) => [...prev, message]); - }); - - const sendMessage = async () => { - if (chatRoom.connection && input.trim()) { - await chatRoom.connection.sendMessage(username, input); - setInput(""); - } - }; - - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - sendMessage(); - } - }; - - return ( -
-
-

Chat Room: {roomId}

-
- -
- - setRoomId(e.target.value)} - placeholder="Enter room name" - /> - - setUsername(e.target.value)} - placeholder="Enter your username" - /> -
- -
- {messages.length === 0 ? ( -
- No messages yet. Start the conversation! -
- ) : ( - messages.map((msg, i) => ( -
-
{msg.sender}
-
{msg.text}
-
- {new Date(msg.timestamp).toLocaleTimeString()} -
-
- )) - )} -
- -
- setInput(e.target.value)} - onKeyPress={handleKeyPress} - placeholder="Type a message..." - disabled={!chatRoom.connection} - /> - -
-
- ); -} diff --git a/examples/freestyle/src/frontend/index.html b/examples/freestyle/src/frontend/index.html deleted file mode 100644 index 91526270f9..0000000000 --- a/examples/freestyle/src/frontend/index.html +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - Chat Room Example - - - -
- - - \ No newline at end of file diff --git a/examples/freestyle/tsconfig.json b/examples/freestyle/tsconfig.json deleted file mode 100644 index d4efe57114..0000000000 --- a/examples/freestyle/tsconfig.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - "target": "esnext", - /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - "lib": ["esnext", "dom"], - /* Specify what JSX code is generated. */ - "jsx": "react-jsx", - - /* Specify what module code is generated. */ - "module": "esnext", - /* Specify how TypeScript looks up a file from a given module specifier. */ - "moduleResolution": "bundler", - /* Specify type package names to be included without being referenced in a source file. */ - "types": ["node", "vite/client"], - /* Enable importing .json files */ - "resolveJsonModule": true, - - /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ - "allowJs": true, - /* Enable error reporting in type-checked JavaScript files. */ - "checkJs": false, - - /* Disable emitting files from a compilation. */ - "noEmit": true, - - /* Ensure that each file can be safely transpiled without relying on other imports. */ - "isolatedModules": true, - /* Allow 'import x from y' when a module doesn't have a default export. */ - "allowSyntheticDefaultImports": true, - /* Ensure that casing is correct in imports. */ - "forceConsistentCasingInFileNames": true, - - /* Enable all strict type-checking options. */ - "strict": true, - - /* Skip type checking all .d.ts files. */ - "skipLibCheck": true - }, - "include": ["src/**/*", "actors/**/*", "tests/**/*"] -} diff --git a/examples/freestyle/tsup.config.js b/examples/freestyle/tsup.config.js deleted file mode 100644 index 1b218cf463..0000000000 --- a/examples/freestyle/tsup.config.js +++ /dev/null @@ -1,16 +0,0 @@ -import { defineConfig } from "tsup"; - -export default defineConfig({ - entry: ["src/backend/server.ts"], - outDir: "dist", - // Vite build is in dist/public/* so we don't want to clean it - clean: false, - bundle: true, - platform: "node", - target: "deno2.5", - external: [], - // Include rivetkit in the bundle since it's local - // Note: this makes bundle much larger and can be - // removed if using a version of rivetkit from npm - noExternal: ["rivetkit", "hono"], -}); diff --git a/examples/freestyle/turbo.json b/examples/freestyle/turbo.json deleted file mode 100644 index 29d4cb2625..0000000000 --- a/examples/freestyle/turbo.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "extends": ["//"] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1a43c6f47..099dc54305 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -292,6 +292,67 @@ importers: specifier: ^3.1.1 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + examples/ai-and-user-generated-actors-freestyle: + dependencies: + '@hono/node-server': + specifier: ^1.13.8 + version: 1.19.1(hono@4.9.8) + '@monaco-editor/react': + specifier: ^4.7.0 + version: 4.7.0(monaco-editor@0.55.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rivet-gg/cloud': + specifier: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@715f221 + version: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@715f221 + '@rivetkit/engine-api-full': + specifier: ^25.7.2 + version: 25.8.1 + dotenv: + specifier: ^17.2.2 + version: 17.2.3 + execa: + specifier: ^9.5.2 + version: 9.6.1 + freestyle-sandboxes: + specifier: ^0.0.95 + version: 0.0.95(expo-constants@18.0.10)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@18.3.1(react@18.3.1))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.28.5)(@types/react@19.2.2)(react@18.3.1))(react@18.3.1))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.28.5)(@types/react@19.2.2)(react@18.3.1))(react@18.3.1))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@19.2.2)(react@18.3.1))(ws@8.18.3) + hono: + specifier: ^4.6.0 + version: 4.9.8 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + devDependencies: + '@types/node': + specifier: ^22.13.9 + version: 22.19.1 + '@types/react': + specifier: ^19 + version: 19.2.2 + '@types/react-dom': + specifier: ^19 + version: 19.2.2(@types/react@19.2.2) + '@vitejs/plugin-react': + specifier: ^4.2.0 + version: 4.7.0(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) + concurrently: + specifier: ^8.2.2 + version: 8.2.2 + tsx: + specifier: ^3.12.7 + version: 3.14.0 + typescript: + specifier: ^5.5.2 + version: 5.9.3 + vite: + specifier: ^5.0.0 + version: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + examples/chat-room: dependencies: rivetkit: @@ -684,70 +745,6 @@ importers: specifier: ^5.5.2 version: 5.9.2 - examples/freestyle: - dependencies: - hono: - specifier: 4.9.8 - version: 4.9.8 - rivetkit: - specifier: workspace:* - version: link:../../rivetkit-typescript/packages/rivetkit - devDependencies: - '@rivetkit/engine-api-full': - specifier: ^25.7.2 - version: 25.8.1 - '@rivetkit/react': - specifier: workspace:* - version: link:../../rivetkit-typescript/packages/react - '@types/node': - specifier: ^22.13.9 - version: 22.18.1 - '@types/prompts': - specifier: ^2 - version: 2.4.9 - '@types/react': - specifier: ^19 - version: 19.2.2 - '@types/react-dom': - specifier: ^19 - version: 19.2.2(@types/react@19.2.2) - '@vitejs/plugin-react': - specifier: ^4.2.0 - version: 4.7.0(vite@5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) - concurrently: - specifier: ^8.2.2 - version: 8.2.2 - dotenv: - specifier: ^17.2.2 - version: 17.2.3 - freestyle-sandboxes: - specifier: ^0.0.95 - version: 0.0.95(expo-constants@18.0.10)(expo-linking@7.0.5)(expo@54.0.18)(react-dom@18.3.1(react@18.3.1))(react-native-safe-area-context@5.6.1(react-native@0.82.1(@babel/core@7.28.5)(@types/react@19.2.2)(react@18.3.1))(react@18.3.1))(react-native-screens@4.17.1(react-native@0.82.1(@babel/core@7.28.5)(@types/react@19.2.2)(react@18.3.1))(react@18.3.1))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@19.2.2)(react@18.3.1))(ws@8.18.3) - prompts: - specifier: ^2.4.2 - version: 2.4.2 - react: - specifier: ^18.2.0 - version: 18.3.1 - react-dom: - specifier: ^18.2.0 - version: 18.3.1(react@18.3.1) - tsup: - specifier: ^8.5.0 - version: 8.5.0(@microsoft/api-extractor@7.53.2(@types/node@22.18.1))(jiti@1.21.7)(postcss@8.5.6)(tsx@3.14.0)(typescript@5.9.2)(yaml@2.8.1) - tsx: - specifier: ^3.12.7 - version: 3.14.0 - typescript: - specifier: ^5.5.2 - version: 5.9.2 - vite: - specifier: ^5.0.0 - version: 5.4.20(@types/node@22.18.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) - vitest: - specifier: ^3.1.1 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) - examples/hono: dependencies: '@hono/node-server': @@ -5408,6 +5405,16 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@monaco-editor/loader@1.7.0': + resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + peerDependencies: + monaco-editor: '>= 0.25.0 < 1' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -6421,6 +6428,10 @@ packages: resolution: {tarball: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@11dea2c} version: 0.0.0 + '@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@715f221': + resolution: {tarball: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@715f221} + version: 0.0.0 + '@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@bf2ebb2': resolution: {tarball: https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@bf2ebb2} version: 0.0.0 @@ -6726,6 +6737,9 @@ packages: '@scure/bip39@1.6.0': resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@sentry-internal/browser-utils@8.55.0': resolution: {integrity: sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw==} engines: {node: '>=14.18'} @@ -6866,6 +6880,10 @@ packages: resolution: {integrity: sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA==} engines: {node: '>=18'} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@sindresorhus/slugify@3.0.0': resolution: {integrity: sha512-SCrKh1zS96q+CuH5GumHcyQEVPsM4Ve8oE0E6tw7AAhGq50K8ojbTUOQnX/j9Mhcv/AXiIsbCfquovyGOo5fGw==} engines: {node: '>=20'} @@ -7549,9 +7567,23 @@ packages: '@vitest/expect@1.6.1': resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@3.2.4': resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} peerDependencies: @@ -7563,6 +7595,9 @@ packages: vite: optional: true + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@3.1.1': resolution: {integrity: sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==} @@ -7572,18 +7607,27 @@ packages: '@vitest/runner@1.6.1': resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@3.2.4': resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} '@vitest/snapshot@1.6.1': resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@3.2.4': resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} '@vitest/spy@1.6.1': resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} @@ -7595,6 +7639,9 @@ packages: '@vitest/utils@1.6.1': resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@3.1.1': resolution: {integrity: sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==} @@ -8838,6 +8885,9 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} @@ -9345,6 +9395,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + exit-hook@2.2.1: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} @@ -9559,6 +9613,10 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -9785,6 +9843,10 @@ packages: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -10030,6 +10092,10 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} @@ -10248,6 +10314,10 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -10260,6 +10330,10 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + is-url@1.2.4: resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} @@ -10809,6 +10883,11 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} @@ -11216,6 +11295,9 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + monaco-editor@0.55.1: + resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} + motion-dom@11.18.1: resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==} @@ -11411,6 +11493,10 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} @@ -11617,6 +11703,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parse-node-version@1.0.1: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} @@ -11976,6 +12066,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + proc-log@4.2.0: resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -12816,6 +12910,9 @@ packages: resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} engines: {node: '>=6'} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'} @@ -12915,6 +13012,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -13131,6 +13232,10 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -13139,6 +13244,10 @@ packages: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tinyspy@4.0.4: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} @@ -13434,6 +13543,10 @@ packages: resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} engines: {node: '>=4'} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -13633,6 +13746,11 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -13809,6 +13927,31 @@ packages: jsdom: optional: true + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@3.2.4: resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -14124,6 +14267,10 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + youch-core@0.3.3: resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} @@ -17147,6 +17294,17 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@monaco-editor/loader@1.7.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@monaco-editor/loader': 1.7.0 + monaco-editor: 0.55.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.7.1 @@ -18276,6 +18434,19 @@ snapshots: transitivePeerDependencies: - encoding + '@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@715f221': + dependencies: + cross-fetch: 4.1.0 + form-data: 4.0.5 + js-base64: 3.7.8 + node-fetch: 2.7.0 + pino-pretty: 13.1.2 + qs: 6.14.0 + readable-stream: 4.7.0 + url-join: 5.0.0 + transitivePeerDependencies: + - encoding + '@rivet-gg/cloud@https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@bf2ebb2': dependencies: cross-fetch: 4.1.0 @@ -18291,7 +18462,7 @@ snapshots: '@rivetkit/engine-api-full@25.8.1': dependencies: - form-data: 4.0.4 + form-data: 4.0.5 js-base64: 3.7.8 node-fetch: 2.7.0 qs: 6.14.0 @@ -18643,6 +18814,8 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 + '@sec-ant/readable-stream@0.4.1': {} + '@sentry-internal/browser-utils@8.55.0': dependencies: '@sentry/core': 8.55.0 @@ -18820,6 +18993,8 @@ snapshots: '@sindresorhus/is@7.1.0': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@sindresorhus/slugify@3.0.0': dependencies: '@sindresorhus/transliterate': 2.2.0 @@ -19302,7 +19477,7 @@ snapshots: '@types/prompts@2.4.9': dependencies: - '@types/node': 22.18.1 + '@types/node': 22.19.1 kleur: 3.0.3 '@types/qs@6.9.8': {} @@ -19681,6 +19856,13 @@ snapshots: '@vitest/utils': 1.6.1 chai: 4.5.0 + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.3 @@ -19689,6 +19871,14 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 + '@vitest/mocker@2.1.9(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + '@vitest/mocker@3.2.4(vite@5.4.20(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0))': dependencies: '@vitest/spy': 3.2.4 @@ -19729,6 +19919,10 @@ snapshots: optionalDependencies: vite: 5.4.20(@types/node@24.7.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.1.1': dependencies: tinyrainbow: 2.0.0 @@ -19743,6 +19937,11 @@ snapshots: p-limit: 5.0.0 pathe: 1.1.2 + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/runner@3.2.4': dependencies: '@vitest/utils': 3.2.4 @@ -19755,6 +19954,12 @@ snapshots: pathe: 1.1.2 pretty-format: 29.7.0 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.19 + pathe: 1.1.2 + '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 @@ -19765,6 +19970,10 @@ snapshots: dependencies: tinyspy: 2.2.1 + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.4 @@ -19787,6 +19996,12 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + '@vitest/utils@3.1.1': dependencies: '@vitest/pretty-format': 3.1.1 @@ -21142,6 +21357,10 @@ snapshots: '@babel/runtime': 7.28.4 csstype: 3.1.3 + dompurify@3.2.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dot-case@3.0.4: dependencies: no-case: 3.0.4 @@ -21859,6 +22078,21 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + exit-hook@2.2.1: {} expand-template@2.0.3: {} @@ -22143,6 +22377,10 @@ snapshots: fflate@0.8.2: {} + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -22414,6 +22652,11 @@ snapshots: get-stream@8.0.1: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -22767,6 +23010,8 @@ snapshots: human-signals@5.0.0: {} + human-signals@8.0.1: {} + humanize-ms@1.2.1: dependencies: ms: 2.1.3 @@ -22958,6 +23203,8 @@ snapshots: is-stream@3.0.0: {} + is-stream@4.0.1: {} + is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -22973,6 +23220,8 @@ snapshots: dependencies: which-typed-array: 1.1.19 + is-unicode-supported@2.1.0: {} + is-url@1.2.4: {} is-weakmap@2.0.2: {} @@ -23505,6 +23754,8 @@ snapshots: markdown-table@3.0.4: {} + marked@14.0.0: {} + marky@1.3.0: {} math-intrinsics@1.1.0: {} @@ -24400,6 +24651,11 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 + monaco-editor@0.55.1: + dependencies: + dompurify: 3.2.7 + marked: 14.0.0 + motion-dom@11.18.1: dependencies: motion-utils: 11.18.1 @@ -24596,6 +24852,11 @@ snapshots: dependencies: path-key: 4.0.0 + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + nullthrows@1.1.1: {} ob1@0.83.2: @@ -24849,6 +25110,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-ms@4.0.0: {} + parse-node-version@1.0.1: {} parse-png@2.1.0: @@ -25016,15 +25279,6 @@ snapshots: optionalDependencies: postcss: 8.5.6 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@3.14.0)(yaml@2.8.1): - dependencies: - lilconfig: 3.1.3 - optionalDependencies: - jiti: 1.21.7 - postcss: 8.5.6 - tsx: 3.14.0 - yaml: 2.8.1 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.5)(yaml@2.8.1): dependencies: lilconfig: 3.1.3 @@ -25161,6 +25415,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + proc-log@4.2.0: {} process-nextick-args@2.0.1: {} @@ -26347,6 +26605,8 @@ snapshots: dependencies: type-fest: 0.7.1 + state-local@1.0.7: {} + statuses@1.5.0: {} statuses@2.0.1: {} @@ -26465,6 +26725,8 @@ snapshots: strip-final-newline@3.0.0: {} + strip-final-newline@4.0.0: {} + strip-json-comments@2.0.1: {} strip-json-comments@3.1.1: {} @@ -26754,10 +27016,14 @@ snapshots: tinypool@1.1.1: {} + tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} tinyspy@2.2.1: {} + tinyspy@3.0.2: {} + tinyspy@4.0.4: {} tm-themes@1.10.12: {} @@ -26850,35 +27116,6 @@ snapshots: - tsx - yaml - tsup@8.5.0(@microsoft/api-extractor@7.53.2(@types/node@22.18.1))(jiti@1.21.7)(postcss@8.5.6)(tsx@3.14.0)(typescript@5.9.2)(yaml@2.8.1): - dependencies: - bundle-require: 5.1.0(esbuild@0.25.9) - cac: 6.7.14 - chokidar: 4.0.3 - consola: 3.4.2 - debug: 4.4.1 - esbuild: 0.25.9 - fix-dts-default-cjs-exports: 1.0.1 - joycon: 3.1.1 - picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@3.14.0)(yaml@2.8.1) - resolve-from: 5.0.0 - rollup: 4.50.1 - source-map: 0.8.0-beta.0 - sucrase: 3.35.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tree-kill: 1.2.2 - optionalDependencies: - '@microsoft/api-extractor': 7.53.2(@types/node@22.18.1) - postcss: 8.5.6 - typescript: 5.9.2 - transitivePeerDependencies: - - jiti - - supports-color - - tsx - - yaml - tsup@8.5.0(@microsoft/api-extractor@7.53.2(@types/node@22.18.1))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1): dependencies: bundle-require: 5.1.0(esbuild@0.25.9) @@ -27209,6 +27446,8 @@ snapshots: unicode-property-aliases-ecmascript@2.2.0: {} + unicorn-magic@0.3.0: {} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -27502,6 +27741,24 @@ snapshots: - supports-color - terser + vite-node@2.1.9(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@3.2.4(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: cac: 6.7.14 @@ -27860,6 +28117,41 @@ snapshots: - supports-color - terser + vitest@2.1.9(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.19 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.20(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + vite-node: 2.1.9(@types/node@22.19.1)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.13)(less@4.4.1)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.62.0)(terser@5.44.0): dependencies: '@types/chai': 5.2.3 @@ -28370,6 +28662,8 @@ snapshots: yocto-queue@1.2.1: {} + yoctocolors@2.1.2: {} + youch-core@0.3.3: dependencies: '@poppinss/exception': 1.2.2 diff --git a/website/public/llms-full.txt b/website/public/llms-full.txt index af3dfd6eda..6744fdbd6a 100644 --- a/website/public/llms-full.txt +++ b/website/public/llms-full.txt @@ -182,6 +182,262 @@ See [helper types](/docs/actors/helper-types) for more details on using `ActionC - [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for defining actors with actions - [`ActorHandle`](/typedoc/types/rivetkit.client_mod.ActorHandle.html) - Handle for calling actions from client - [`ActorActionFunction`](/typedoc/types/rivetkit.client_mod.ActorActionFunction.html) - Type for action functions +## AI and User-Generated Rivet Actors + +# AI and User-Generated Rivet Actors + +Deploy AI or user-generated Rivet Actor code with sandboxed namespaces and serverless infrastructure. + +Complete example showing how to deploy user-generated Rivet Actor code. + +## Overview + +This guide shows you how to programmatically create isolated Rivet environments and deploy custom actor code to them. This pattern is useful for: + +- **AI-generated code deployments** - Deploy code generated by LLMs in isolated environments +- **User sandbox environments** - Give users their own isolated Rivet namespace to experiment +- **Preview deployments** - Create ephemeral environments for testing pull requests +- **Multi-tenant applications** - Isolate each customer in their own namespace + +## How It Works + +The deployment process involves four key steps: + +1. **Create sandboxed Rivet namespace** - Programmatically create an isolated Rivet namespace using the Cloud API or self-hosted Rivet API +2. **Generate tokens** - Create the necessary tokens for authentication (runner, publishable, and access tokens) +3. **Deploy to Freestyle** - Deploy the actor code and frontend to Freestyle's serverless platform +4. **Configure runner** - Connect Rivet to the Freestyle deployment so actors can be executed + +## Sandboxed Rivet Namespaces + +Rivet provides the ability to create sandboxed namespaces programmatically. Each namespace is completely isolated with its own: + +- Actor instances and state +- Authentication tokens +- Configuration settings +- Resource limits + +Namespaces can be created on-demand through the Rivet API, making them perfect for scenarios where you need to quickly spin up isolated environments. + +## Deploying Code to Freestyle + +[Freestyle](https://docs.freestyle.sh/web/overview) is a serverless platform optimized for deploying JavaScript/TypeScript code, particularly code written by users or AI systems. It provides: + +- Fast cold starts (around 5ms) +- Automatic TLS certificates +- Built-in security for untrusted code +- Easy deployment API + +We use Freestyle to host the backend logic for your Rivet Actors, making it easy to deploy custom code without managing infrastructure. + +## Setup + + 1. Visit your project on [Rivet Cloud](https://dashboard.rivet.dev/) + 2. Click on "Tokens" in the sidebar + 3. Under "Cloud API Tokens" click "Create Token" + 4. Copy the token for use in your deployment script + + Create a deployment script that handles namespace creation, token generation, Freestyle deployment, and runner configuration. + + This code demonstrates the complete flow for deploying user-generated Rivet Actor code to Freestyle with Rivet Cloud: + + ```typescript + // Configuration + const CLOUD_API_URL = "https://api.rivet.dev/cloud"; + const CLOUD_API_TOKEN = "your-cloud-api-token"; + const ENGINE_ENDPOINT = "https://api.rivet.dev"; + const FREESTYLE_DOMAIN = "your-app.style.dev"; + const FREESTYLE_API_KEY = "your-freestyle-api-key"; + const DATACENTER = "us-west-1"; + + async function deploy(registryCode: string, appCode: string) ); + + const = await cloudRivet.apiTokens.inspect(); + const namespaceName = `ns-$-$`; + + const = await cloudRivet.namespaces.create(project, ); + + // Step 2: Generate tokens + const = await cloudRivet.namespaces.createSecretToken( + project, + namespace.name, + -runner-token`, + org: organization, + }, + ); + + const = await cloudRivet.namespaces.createPublishableToken( + project, + namespace.name, + , + ); + + const = await cloudRivet.namespaces.createAccessToken( + project, + namespace.name, + , + ); + + // Step 3: Deploy to Freestyle + // Build your project directory with the custom registry and app code + // See the example repository for the complete template structure + const freestyle = new FreestyleSandboxes(); + const deploymentSource = prepareDirForDeploymentSync(projectDir); + + const = await freestyle.deployWeb(deploymentSource, `, + RIVET_RUNNER_KIND: "serverless", + VITE_RIVET_ENDPOINT: ENGINE_ENDPOINT, + VITE_RIVET_NAMESPACE: namespace.access.engineNamespaceName, + VITE_RIVET_TOKEN: publishableToken, + VITE_RIVET_DATACENTER: DATACENTER, + RIVET_ENDPOINT: ENGINE_ENDPOINT, + RIVET_NAMESPACE: namespace.access.engineNamespaceName, + RIVET_RUNNER_TOKEN: runnerToken, + RIVET_PUBLISHABLE_TOKEN: publishableToken, + }, + timeout: 60 * 5, + entrypoint: "src/backend/server.ts", + domains: [FREESTYLE_DOMAIN], + build: false, + }); + + // Step 4: Configure Rivet runner + const engineRivet = new RivetClient(); + + await engineRivet.runnerConfigsUpsert("default", /api/rivet`, + headers: , + runnersMargin: 1, + minRunners: 1, + maxRunners: 1, + slotsPerRunner: 1, + requestLifespan: 60 * 4 + 30, + }, + }, + }, + namespace: namespace.access.engineNamespaceName, + }); + + return /projects/$/ns/$`, + freestyleUrl: `https://admin.freestyle.sh/dashboard/deployments/$`, + }; + } + ``` + + **Step 1: Create sandboxed namespace** - Uses the Cloud API to create a new isolated namespace with a unique name. + + **Step 2: Generate tokens** - Creates three types of tokens: runner token (for executing actors), publishable token (for frontend clients), and access token (for API access). + + **Step 3: Deploy to Freestyle** - Deploys the actor code and frontend to Freestyle with all necessary environment variables configured. The deployment uses a 5-minute timeout to handle long-running operations. + + **Step 4: Configure runner** - Configures Rivet to route actor execution requests to the Freestyle deployment. The runner config specifies scaling limits and request lifespans. + + See the [example repository](https://github.com/rivet-dev/rivet/tree/main/examples/ai-and-user-generated-actors-freestyle) for the complete project structure including the template directory and build process. + + For self-hosted Rivet, you'll need: + - Your Rivet Engine endpoint (e.g., `http://your-rivet-instance:6420`) + - An API token with permissions to create namespaces + + See the [self-hosting documentation](/docs/self-hosting) for details on setting up authentication. + + Create a deployment script for self-hosted Rivet. + + This code demonstrates the complete flow for deploying user-generated Rivet Actor code to Freestyle with self-hosted Rivet: + + ```typescript + // Configuration + const RIVET_ENDPOINT = "http://your-rivet-instance:6420"; + const RIVET_TOKEN = "your-rivet-token"; + const FREESTYLE_DOMAIN = "your-app.style.dev"; + const FREESTYLE_API_KEY = "your-freestyle-api-key"; + const DATACENTER = "us-west-1"; + + async function deploy(registryCode: string, appCode: string) ); + + const namespaceName = `ns-$-$`; + + const = await rivet.namespaces.create(); + + // Step 2: Generate tokens (use the same token for self-hosted) + // For self-hosted, you typically use the same token for all operations + const token = RIVET_TOKEN; + + // Step 3: Deploy to Freestyle + // Build your project directory with the custom registry and app code + // See the example repository for the complete template structure + const freestyle = new FreestyleSandboxes(); + const deploymentSource = prepareDirForDeploymentSync(projectDir); + + const = await freestyle.deployWeb(deploymentSource, `, + RIVET_RUNNER_KIND: "serverless", + VITE_RIVET_ENDPOINT: RIVET_ENDPOINT, + VITE_RIVET_NAMESPACE: namespace.name, + VITE_RIVET_TOKEN: token, + VITE_RIVET_DATACENTER: DATACENTER, + RIVET_ENDPOINT: RIVET_ENDPOINT, + RIVET_NAMESPACE: namespace.name, + RIVET_RUNNER_TOKEN: token, + RIVET_PUBLISHABLE_TOKEN: token, + }, + timeout: 60 * 5, + entrypoint: "src/backend/server.ts", + domains: [FREESTYLE_DOMAIN], + build: false, + }); + + // Step 4: Configure Rivet runner + await rivet.runnerConfigsUpsert("default", /api/rivet`, + headers: , + runnersMargin: 1, + minRunners: 1, + maxRunners: 1, + slotsPerRunner: 1, + requestLifespan: 60 * 4 + 30, + }, + }, + }, + namespace: namespace.name, + }); + + return `, + }; + } + ``` + + **Step 1: Create sandboxed namespace** - Creates a new isolated namespace using the self-hosted Rivet API. + + **Step 2: Generate tokens** - For self-hosted Rivet, you typically use the same token for all operations. The token system is simpler than Rivet Cloud. + + **Step 3: Deploy to Freestyle** - Deploys the actor code and frontend to Freestyle with environment variables pointing to your self-hosted Rivet instance. + + **Step 4: Configure runner** - Configures your self-hosted Rivet to route actor execution requests to the Freestyle deployment. + + See the [example repository](https://github.com/rivet-dev/rivet/tree/main/examples/ai-and-user-generated-actors-freestyle) for the complete project structure including the template directory and build process. + +## Project Structure + +The example uses this structure: + +``` +src/ + backend/ # Backend used to deploy sandboxed Rivet code + frontend/ # Frontend for the deploy UI +template/ # Template Rivet project to deploy + src/ + backend/ # Actor code (registry.ts gets replaced with user code) + frontend/ # Frontend code (App.tsx gets replaced with user code) +``` + +The deployment script: +1. Copies the template directory +2. Replaces `registry.ts` and `App.tsx` with user-provided code +3. Builds and deploys to Freestyle +4. Configures Rivet to connect to the deployment + +## Next Steps + +- Explore the [complete example](https://github.com/rivet-dev/rivet/tree/main/examples/ai-and-user-generated-actors-freestyle) +- Learn more about [Freestyle deployment](https://docs.freestyle.sh/web/overview) +- Read about [Rivet namespaces and tokens](/docs/actors/authentication) ## Authentication # Authentication diff --git a/website/public/llms.txt b/website/public/llms.txt index 226f7ec025..174fb1da1b 100644 --- a/website/public/llms.txt +++ b/website/public/llms.txt @@ -66,6 +66,7 @@ https://rivet.dev/changelog/godot-multiplayer-compared-to-unity https://rivet.dev/cloud https://rivet.dev/docs/actors https://rivet.dev/docs/actors/actions +https://rivet.dev/docs/actors/ai-and-user-generated-actors https://rivet.dev/docs/actors/authentication https://rivet.dev/docs/actors/clients https://rivet.dev/docs/actors/communicating-between-actors diff --git a/website/src/components/CodeBlock.tsx b/website/src/components/CodeBlock.tsx index 24f262b8f1..f8cdbf55b2 100644 --- a/website/src/components/CodeBlock.tsx +++ b/website/src/components/CodeBlock.tsx @@ -17,6 +17,7 @@ const LANGS: shiki.BundledLanguage[] = [ "powershell", "ts", "typescript", + "sql", "yaml", "http", "prisma", diff --git a/website/src/components/v2/Code.tsx b/website/src/components/v2/Code.tsx index 0595774f6e..8466f7a014 100644 --- a/website/src/components/v2/Code.tsx +++ b/website/src/components/v2/Code.tsx @@ -32,6 +32,7 @@ const languageNames = { ruby: "Ruby", ts: "TypeScript", typescript: "TypeScript", + sql: "SQL", yaml: "YAML", gdscript: "GDScript", powershell: "Command Line", diff --git a/website/src/content/docs/actors/ai-and-user-generated-actors.mdx b/website/src/content/docs/actors/ai-and-user-generated-actors.mdx new file mode 100644 index 0000000000..a1fade23bf --- /dev/null +++ b/website/src/content/docs/actors/ai-and-user-generated-actors.mdx @@ -0,0 +1,297 @@ +import { faGithub } from "@rivet-gg/icons"; + +# AI and User-Generated Rivet Actors + +This guide shows you how to programmatically create sandboxed Rivet environments and deploy custom actor code to them. + + + +Complete example showing how to deploy user-generated Rivet Actor code. + + + +## Use Cases + +Deploying AI and user-generated Rivet Actors to sandboxed namespaces is useful for: + +- **AI-generated code deployments**: Deploy code generated by LLMs in sandboxed environments +- **User sandbox environments**: Give users their own sandboxed Rivet namespace to experiment +- **Preview deployments**: Create ephemeral environments for testing pull requests +- **Multi-tenant applications**: Isolate each customer in their own sandboxed namespace + +## Rivet Actors For AI-Generated Backends + +Traditional architectures require AI agents to coordinate across multiple disconnected systems: a database schemas, API logic, and synchronizing schemas & APIs. + +With Rivet Actors, **state and logic live together in a single actor definition**. This consolidation means: + +- **Less LLM context required**: No need to understand multiple systems or keep them in sync +- **Fewer errors**: State and behavior can't drift apart when they're defined together +- **More powerful generation**: AI agents can focus on business logic instead of infrastructure plumbing + +## How It Works + +The deployment process involves four key steps: + +1. **Create sandboxed Rivet namespace**: Programmatically create a sandboxed Rivet namespace using the Cloud API or self-hosted Rivet API +2. **Generate tokens**: Create the necessary tokens for authentication: + - **Runner token**: Authenticates the serverless runner to execute actors + - **Publishable token**: Used by frontend clients to connect to actors + - **Access token**: Provides API access for configuring the namespace +3. **Deploy AI or user-generated code**: Deploy the actor code and frontend programmatically to your serverless platform of choice (such as Vercel, Netlify, AWS Lambda, or any other provider). We'll be using [Freestyle](https://freestyle.sh) for this example since it's built for this use case. +4. **Connect Rivet to your deployed code**: Configure Rivet to run actors on your deployment in your sandboxed namespace + +## Setup + + + + + + Before you begin, ensure you have: + - Node.js 18+ installed + - A [Freestyle](https://freestyle.sh) account and API token + - A [Rivet Cloud](https://dashboard.rivet.dev/) account + + + + 1. Visit your project on [Rivet Cloud](https://dashboard.rivet.dev/) + 2. Click on "Tokens" in the sidebar + 3. Under "Cloud API Tokens" click "Create Token" + 4. Copy the token for use in your deployment script + + + + Install the required dependencies: + + ```bash + npm install @rivetkit/engine-api-full@^25.7.2 freestyle-sandboxes@^0.0.95 + ``` + + + + Write deployment code that handles namespace creation, token generation, Freestyle deployment, and runner configuration. This can be called from your backend to deploy actor and frontend code to an isolated Rivet namespace. + + ```typescript + import { execSync } from "child_process"; + import { RivetClient } from "@rivetkit/engine-api-full"; + import { FreestyleSandboxes } from "freestyle-sandboxes"; + import { prepareDirForDeploymentSync } from "freestyle-sandboxes/utils"; + + const CLOUD_API_TOKEN = "your-cloud-api-token"; + const FREESTYLE_DOMAIN = "your-app.style.dev"; + const FREESTYLE_API_KEY = "your-freestyle-api-key"; + + async function deploy(projectDir: string) { + // Step 1: Inspect API token to get project and organization + const { project, organization } = await cloudRequest("GET", "/tokens/api/inspect"); + + // Step 2: Create sandboxed namespace with a unique name + const namespaceName = `ns-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; + + const { namespace } = await cloudRequest( + "POST", + `/projects/${project}/namespaces?org=${organization}`, + { displayName: namespaceName.substring(0, 16) }, + ); + const engineNamespaceName = namespace.access.engineNamespaceName; // NOTE: Intentionally different than namespace.name + + // Step 3: Generate tokens + // - Runner token: authenticates the serverless runner to execute actors + // - Publishable token: used by frontend clients to connect to actors + // - Access token: provides API access for configuring the namespace + const { token: runnerToken } = await cloudRequest( + "POST", + `/projects/${project}/namespaces/${namespace.name}/tokens/secret?org=${organization}`, + ); + + const { token: publishableToken } = await cloudRequest( + "POST", + `/projects/${project}/namespaces/${namespace.name}/tokens/publishable?org=${organization}`, + ); + + const { token: accessToken } = await cloudRequest( + "POST", + `/projects/${project}/namespaces/${namespace.name}/tokens/access?org=${organization}`, + ); + + // Step 4: Build the frontend with public environment variables. + execSync("npm run build", { + cwd: projectDir, + env: { + ...process.env, + VITE_RIVET_ENDPOINT: "https://api.rivet.dev", + VITE_RIVET_NAMESPACE: engineNamespaceName, + VITE_RIVET_TOKEN: publishableToken, + }, + stdio: "inherit", + }); + + // Step 5: Deploy actor code and frontend to Freestyle with backend + // environment variables. + const freestyle = new FreestyleSandboxes({ apiKey: FREESTYLE_API_KEY }); + const deploymentSource = prepareDirForDeploymentSync(projectDir); + + const { deploymentId } = await freestyle.deployWeb(deploymentSource, { + envVars: { + RIVET_ENDPOINT: "https://api.rivet.dev", + RIVET_NAMESPACE: engineNamespaceName, + RIVET_TOKEN: runnerToken, + }, + entrypoint: "src/backend/server.ts", + domains: [FREESTYLE_DOMAIN], + build: false, + }); + + // Step 6: Configure Rivet to run actors on the Freestyle deployment. + const rivet = new RivetClient({ + environment: "https://api.rivet.dev", + token: accessToken, + }); + + await rivet.runnerConfigsUpsert("default", { + datacenters: { + "us-west-1": { // Freestyle datacenter is on west coast + serverless: { + url: `https://${FREESTYLE_DOMAIN}/api/rivet`, + headers: {}, + runnersMargin: 0, + minRunners: 0, + maxRunners: 1000, + slotsPerRunner: 1, + requestLifespan: 60 * 5, + }, + }, + }, + namespace: engineNamespaceName, + }); + + console.log("Deployment complete!"); + console.log("Frontend:", `https://${FREESTYLE_DOMAIN}`); + console.log("Rivet Dashboard:", `https://dashboard.rivet.dev/orgs/${organization}/projects/${project}/ns/${namespace.name}`); + console.log("Freestyle Dashboard:", `https://admin.freestyle.sh/dashboard/deployments/${deploymentId}`); + } + + async function cloudRequest(method: string, path: string, body?: any) { + const res = await fetch(`https://api-cloud.rivet.dev${path}`, { + method, + headers: { + Authorization: `Bearer ${CLOUD_API_TOKEN}`, + ...(body && { "Content-Type": "application/json" }), + }, + ...(body && { body: JSON.stringify(body) }), + }); + return res.json(); + } + ``` + + See the [example repository](https://github.com/rivet-dev/rivet/tree/main/examples/ai-and-user-generated-actors-freestyle) for the complete project structure including the template directory and build process. + + For more information on Freestyle deployment, see the [Freestyle documentation](https://docs.freestyle.sh/web/overview). + + + + + + + + Before you begin, ensure you have: + - Node.js 18+ installed + - A [Freestyle](https://freestyle.sh) account and API key + - A [self-hosted Rivet instance](/docs/self-hosting) with endpoint and API token + + + + Install the required dependencies: + + ```bash + npm install @rivetkit/engine-api-full@^25.7.2 freestyle-sandboxes@^0.0.95 + ``` + + + + Write deployment code that handles namespace creation, Freestyle deployment, and runner configuration. This can be called from your backend to deploy actor and frontend code to an isolated Rivet namespace. + + ```typescript + import { execSync } from "child_process"; + import { RivetClient } from "@rivetkit/engine-api-full"; + import { FreestyleSandboxes } from "freestyle-sandboxes"; + import { prepareDirForDeploymentSync } from "freestyle-sandboxes/utils"; + + // Configuration + const RIVET_ENDPOINT = "http://your-rivet-instance:6420"; + const RIVET_TOKEN = "your-rivet-token"; + const FREESTYLE_DOMAIN = "your-app.style.dev"; + const FREESTYLE_API_KEY = "your-freestyle-api-key"; + + async function deploy(projectDir: string) { + // Step 1: Create sandboxed namespace using the self-hosted Rivet API + const rivet = new RivetClient({ + environment: RIVET_ENDPOINT, + token: RIVET_TOKEN, + }); + + const namespaceName = `ns-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; + + const { namespace } = await rivet.namespaces.create({ + displayName: namespaceName, + name: namespaceName, + }); + + // Step 2: Build the frontend with public environment variables. + execSync("npm run build", { + cwd: projectDir, + env: { + ...process.env, + VITE_RIVET_ENDPOINT: RIVET_ENDPOINT, + VITE_RIVET_NAMESPACE: namespace.name, + VITE_RIVET_TOKEN: RIVET_TOKEN, + }, + stdio: "inherit", + }); + + // Step 3: Deploy actor and frontend to Freestyle with backend + // environment variables. + const freestyle = new FreestyleSandboxes({ apiKey: FREESTYLE_API_KEY }); + const deploymentSource = prepareDirForDeploymentSync(projectDir); + + const { deploymentId } = await freestyle.deployWeb(deploymentSource, { + envVars: { + RIVET_ENDPOINT, + RIVET_NAMESPACE: namespace.name, + RIVET_TOKEN, + }, + entrypoint: "src/backend/server.ts", + domains: [FREESTYLE_DOMAIN], + build: false, + }); + + // Step 4: Configure your self-hosted Rivet to run actors on the Freestyle + // deployment + await rivet.runnerConfigsUpsert("default", { + datacenters: { + "us-west-1": { // Freestyle datacenter is on west coast + serverless: { + url: `https://${FREESTYLE_DOMAIN}/api/rivet`, + headers: {}, + runnersMargin: 0, + minRunners: 0, + maxRunners: 1000, + slotsPerRunner: 1, + requestLifespan: 60 * 5, + }, + }, + }, + namespace: namespace.name, + }); + + console.log("Deployment complete!"); + console.log("Frontend:", `https://${FREESTYLE_DOMAIN}`); + console.log("Freestyle Dashboard:", `https://admin.freestyle.sh/dashboard/deployments/${deploymentId}`); + } + ``` + + See the [example repository](https://github.com/rivet-dev/rivet/tree/main/examples/ai-and-user-generated-actors-freestyle) for the complete project structure including the template directory and build process. + + + + diff --git a/website/src/posts/2025-12-03-ai-generated-backends/image.png b/website/src/posts/2025-12-03-ai-generated-backends/image.png new file mode 100644 index 0000000000..4aa3532400 Binary files /dev/null and b/website/src/posts/2025-12-03-ai-generated-backends/image.png differ diff --git a/website/src/posts/2025-12-03-ai-generated-backends/page.mdx b/website/src/posts/2025-12-03-ai-generated-backends/page.mdx new file mode 100644 index 0000000000..8224e38f04 --- /dev/null +++ b/website/src/posts/2025-12-03-ai-generated-backends/page.mdx @@ -0,0 +1,206 @@ +export const author = "nathan-flurry" +export const published = "2025-12-03" +export const category = "changelog" +export const keywords = ["ai", "backend", "generation", "freestyle", "actors", "realtime", "database"] + +# Generating AI & User Generated Backends with Rivet + +**Rivet now supports programmatically deploying AI-generated and user-generated actor code to sandboxed namespaces, enabling use cases like AI code execution, user sandbox environments, preview deployments, and multi-tenant applications.** + +## The Problem With AI-Generated Backend Code + +When AI agents generate backend code, they typically need to coordinate across multiple disconnected systems: + +- **Database schemas** - Define tables, columns, and relationships +- **API logic** - Write endpoints that query and mutate data +- **Schema synchronization** - Keep database and API in sync as requirements change + +Each system requires separate context, increasing token usage and cognitive load for the AI. More importantly, state and behavior can drift apart when defined separately, leading to bugs and inconsistencies. + +## How We Solved This + +### Rivet Actors: AI-Friendly Code Generation With Less Context & Fragmentation + +Rivet Actors solve this by unifying state and logic in a single actor definition. Instead of coordinating between databases and APIs, **state and behavior live together**: + + + + +```typescript {{"title":"registry.ts"}} +export const user = actor({ + // State is defined alongside behavior + createState: (c, input) => ({ + name: input.name, + email: input.email, + createdAt: Date.now(), + }), + + // Actions can read and mutate state + actions: { + updateEmail: (c, email: string) => { + c.state.email = email; + }, + getProfile: (c) => c.state, + }, +}); +``` + + + + +```sql {{"title":"migrations/001_create_users.sql"}} +CREATE TABLE users ( + id UUID PRIMARY KEY, + name TEXT NOT NULL, + created_at BIGINT NOT NULL +); +``` + +```sql {{"title":"migrations/002_add_email.sql"}} +ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''; +CREATE INDEX idx_users_email ON users(email); +``` + +```typescript {{"title":"api/users.ts"}} +import { Hono } from "hono"; +import { db } from "./db"; + +const app = new Hono(); + +app.post("/users/:id", async (c) => { + const { id } = c.req.param(); + const { name, email } = await c.req.json(); + + await db.query( + "INSERT INTO users (id, name, email, created_at) VALUES ($1, $2, $3, $4)", + [id, name, email, Date.now()] + ); + + return c.json({ success: true }); +}); + +app.patch("/users/:id/email", async (c) => { + const { id } = c.req.param(); + const { email } = await c.req.json(); + + await db.query( + "UPDATE users SET email = $1 WHERE id = $2", + [email, id] + ); + + return c.json({ success: true }); +}); + +app.get("/users/:id", async (c) => { + const { id } = c.req.param(); + const result = await db.query( + "SELECT name, email, created_at FROM users WHERE id = $1", + [id] + ); + + return c.json(result.rows[0]); +}); +``` + + + + +This consolidation eliminates fragmentation: + +- **Single source of truth**: No need to keep migrations, schemas, and APIs in sync +- **Less LLM context required**: Generate one file instead of coordinating multiple systems +- **Fewer errors**: State and behavior can't drift apart when they're defined together +- **More powerful generation**: AI agents can focus on business logic instead of infrastructure plumbing + +### Rivet Namespaces: Fully Sandboxed Environments + +Sandboxed namespaces provide isolated environments where **each AI-generated or user-generated deployment runs independently** with its own resources, tokens, and configuration. This enables safe multi-tenant deployments and user-generated code execution. + +## Use Cases + +Sandboxed namespaces enable a variety of isolated deployment scenarios: + +- **AI-generated code**: Deploy LLM-generated backends safely in isolated environments +- **User sandbox environments**: Give users their own sandboxed Rivet namespace to experiment +- **Preview deployments**: Create ephemeral environments for testing pull requests +- **Multi-tenant applications**: Isolate each customer in their own sandboxed namespace + +## How It Works + +The deployment process involves four key steps: + +1. **Create sandboxed namespace**: Programmatically create an isolated Rivet namespace using the Cloud API or self-hosted Rivet API + +2. **Generate tokens**: Create the necessary authentication tokens: + - **Runner token**: Authenticates the serverless runner to execute actors + - **Publishable token**: Used by frontend clients to connect to actors + - **Access token**: Provides API access for configuring the namespace + +3. **Deploy code**: Deploy the actor code and frontend programmatically to your serverless platform of choice (Vercel, Netlify, AWS Lambda, Freestyle, etc.) + +4. **Connect Rivet**: Configure Rivet to run actors on your deployment in your sandboxed namespace + +## Show Me The Code + +Here's a simplified example of the deployment flow using [Freestyle](https://freestyle.sh) (built specifically for this use case): + +```typescript +import { RivetClient } from "@rivetkit/engine-api-full"; +import { FreestyleSandboxes } from "freestyle-sandboxes"; + +async function deploy(projectDir: string) { + // Step 1: Create sandboxed namespace + const { project, organization } = await cloudRequest("GET", "/tokens/api/inspect"); + const { namespace } = await cloudRequest( + "POST", + `/projects/${project}/namespaces?org=${organization}`, + { displayName: `ns-${Date.now()}` } + ); + + // Step 2: Generate tokens + // ...omitted... + + // Step 3: Deploy to Freestyle + const freestyle = new FreestyleSandboxes({ apiKey: FREESTYLE_API_KEY }); + const deploymentSource = prepareDirForDeploymentSync(projectDir); + await freestyle.deployWeb(deploymentSource, { + envVars: { + RIVET_ENDPOINT: "https://api.rivet.dev", + RIVET_NAMESPACE: namespace.access.engineNamespaceName, + RIVET_TOKEN: runnerToken, + }, + entrypoint: "src/backend/server.ts", + domains: [FREESTYLE_DOMAIN], + }); + + // Step 4: Configure Rivet to run actors on the deployment + const rivet = new RivetClient({ + environment: "https://api.rivet.dev", + token: accessToken, + }); + await rivet.runnerConfigsUpsert("default", { + datacenters: { + "us-west-1": { + serverless: { + url: `https://${FREESTYLE_DOMAIN}/api/rivet`, + headers: {}, + maxRunners: 1000, + }, + }, + }, + namespace: namespace.access.engineNamespaceName, + }); +} +``` + +Call this deployment function whenever your AI agent generates new actor code or a user requests their own sandbox environment. The entire process takes seconds and creates a fully isolated, production-ready deployment. + +## Getting Started + +To try deploying AI-generated or user-generated Rivet Actors: + +- Check out the [complete example on GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/ai-and-user-generated-actors-freestyle) +- Read the [full documentation guide](/docs/actors/ai-and-user-generated-actors) +- Sign up for [Rivet Cloud](https://dashboard.rivet.dev) or [self-host Rivet](/docs/self-hosting) + + diff --git a/website/src/sitemap/mod.ts b/website/src/sitemap/mod.ts index 898712de61..00870410f6 100644 --- a/website/src/sitemap/mod.ts +++ b/website/src/sitemap/mod.ts @@ -250,6 +250,10 @@ export const sitemap = [ href: "/docs/actors/testing", // icon: faVialCircleCheck, }, + { + title: "AI & User-Generated Actors", + href: "/docs/actors/ai-and-user-generated-actors", + }, { title: "Helper Types", href: "/docs/actors/helper-types",