diff --git a/.gitignore b/.gitignore index 3075732..0505140 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,3 @@ -# Theia build files -theia/node_modules/ -theia/packages/*/lib/ -theia/examples/*/lib/ -theia/examples/*/dist/ -theia/*.log -theia/.nyc_output/ -theia/coverage/ -theia/lerna-debug.log* -theia/npm-debug.log* - # OS files .DS_Store .DS_Store? @@ -27,9 +16,16 @@ theia/npm-debug.log* *.tmp .cache/ -# Distribution builds -theia/examples/electron/dist/ - # Environment .env .env.local +.env.* +*.env +.coolify.env +.opensrs.env + +# Dependencies & build artifacts +**/node_modules/ +**/.next/ +**/.turbo/ +**/coverage/ diff --git a/1.Generate Control Plane API scaffold.md b/1.Generate Control Plane API scaffold.md deleted file mode 100644 index d5c2dd6..0000000 --- a/1.Generate Control Plane API scaffold.md +++ /dev/null @@ -1,743 +0,0 @@ -1) Generate Control Plane API scaffold -Folder layout -backend/control-plane/ - package.json - tsconfig.json - src/ - index.ts - config.ts - auth.ts - registry.ts - types.ts - storage/ - firestore.ts - gcs.ts - routes/ - tools.ts - runs.ts - health.ts - .env.example - -backend/control-plane/package.json -{ - "name": "@productos/control-plane", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "dist/index.js", - "scripts": { - "dev": "tsx watch src/index.ts", - "build": "tsc -p tsconfig.json", - "start": "node dist/index.js", - "lint": "eslint ." - }, - "dependencies": { - "@google-cloud/firestore": "^7.11.0", - "@google-cloud/storage": "^7.14.0", - "@fastify/cors": "^9.0.1", - "@fastify/helmet": "^12.0.0", - "@fastify/rate-limit": "^9.1.0", - "fastify": "^4.28.1", - "zod": "^3.23.8", - "nanoid": "^5.0.7" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "tsx": "^4.19.0", - "typescript": "^5.5.4", - "eslint": "^9.8.0" - } -} - -backend/control-plane/tsconfig.json -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "Bundler", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "types": ["node"] - } -} - -backend/control-plane/.env.example -PORT=8080 -GCP_PROJECT_ID=your-project-id -GCS_BUCKET_ARTIFACTS=productos-artifacts-dev -FIRESTORE_COLLECTION_RUNS=runs -FIRESTORE_COLLECTION_TOOLS=tools -# If you put behind IAP / OAuth later, validate ID tokens here: -AUTH_MODE=dev # dev | oauth - -backend/control-plane/src/config.ts -export const config = { - port: Number(process.env.PORT ?? 8080), - projectId: process.env.GCP_PROJECT_ID ?? "", - artifactsBucket: process.env.GCS_BUCKET_ARTIFACTS ?? "", - runsCollection: process.env.FIRESTORE_COLLECTION_RUNS ?? "runs", - toolsCollection: process.env.FIRESTORE_COLLECTION_TOOLS ?? "tools", - authMode: process.env.AUTH_MODE ?? "dev" -}; - -backend/control-plane/src/types.ts -export type ToolRisk = "low" | "medium" | "high"; - -export type ToolDef = { - name: string; - description: string; - risk: ToolRisk; - executor: { - kind: "http"; - url: string; // executor base url - path: string; // executor endpoint path - }; - inputSchema: unknown; // JSON Schema object - outputSchema?: unknown; // JSON Schema object -}; - -export type ToolInvokeRequest = { - tool: string; - tenant_id: string; - workspace_id?: string; - input: unknown; - dry_run?: boolean; -}; - -export type RunStatus = "queued" | "running" | "succeeded" | "failed"; - -export type RunRecord = { - run_id: string; - tenant_id: string; - tool: string; - status: RunStatus; - created_at: string; - updated_at: string; - input: unknown; - output?: unknown; - error?: { message: string; details?: unknown }; - artifacts?: { bucket: string; prefix: string }; -}; - -backend/control-plane/src/auth.ts -import { FastifyRequest } from "fastify"; -import { config } from "./config.js"; - -/** - * V1: dev mode = trust caller (or a shared API key later). - * V2: validate Google OAuth/IAP identity token and map to tenant/org. - */ -export async function requireAuth(req: FastifyRequest) { - if (config.authMode === "dev") return; - - // Placeholder for OAuth/IAP verification: - // - read Authorization: Bearer - // - verify token (Google JWKS) - // - attach req.user - throw new Error("AUTH_MODE oauth not yet implemented"); -} - -backend/control-plane/src/storage/firestore.ts -import { Firestore } from "@google-cloud/firestore"; -import { config } from "../config.js"; -import type { RunRecord, ToolDef } from "../types.js"; - -const db = new Firestore({ projectId: config.projectId }); - -export async function saveRun(run: RunRecord): Promise { - await db.collection(config.runsCollection).doc(run.run_id).set(run, { merge: true }); -} - -export async function getRun(runId: string): Promise { - const snap = await db.collection(config.runsCollection).doc(runId).get(); - return snap.exists ? (snap.data() as RunRecord) : null; -} - -export async function saveTool(tool: ToolDef): Promise { - await db.collection(config.toolsCollection).doc(tool.name).set(tool, { merge: true }); -} - -export async function listTools(): Promise { - const snap = await db.collection(config.toolsCollection).get(); - return snap.docs.map(d => d.data() as ToolDef); -} - -backend/control-plane/src/storage/gcs.ts -import { Storage } from "@google-cloud/storage"; -import { config } from "../config.js"; - -const storage = new Storage({ projectId: config.projectId }); - -export async function writeArtifactText(prefix: string, filename: string, content: string) { - const bucket = storage.bucket(config.artifactsBucket); - const file = bucket.file(`${prefix}/${filename}`); - await file.save(content, { contentType: "text/plain" }); - return { bucket: config.artifactsBucket, path: `${prefix}/${filename}` }; -} - -backend/control-plane/src/registry.ts -import type { ToolDef } from "./types.js"; -import { listTools } from "./storage/firestore.js"; - -/** - * Simple registry. V2: cache + versioning + per-tenant overrides. - */ -export async function getRegistry(): Promise> { - const tools = await listTools(); - return Object.fromEntries(tools.map(t => [t.name, t])); -} - -backend/control-plane/src/routes/health.ts -import type { FastifyInstance } from "fastify"; - -export async function healthRoutes(app: FastifyInstance) { - app.get("/healthz", async () => ({ ok: true })); -} - -backend/control-plane/src/routes/tools.ts -import type { FastifyInstance } from "fastify"; -import { nanoid } from "nanoid"; -import { requireAuth } from "../auth.js"; -import { getRegistry } from "../registry.js"; -import { saveRun } from "../storage/firestore.js"; -import { writeArtifactText } from "../storage/gcs.js"; -import type { RunRecord, ToolInvokeRequest } from "../types.js"; - -async function postJson(url: string, body: unknown) { - const res = await fetch(url, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(body) - }); - if (!res.ok) { - const txt = await res.text(); - throw new Error(`Executor error ${res.status}: ${txt}`); - } - return res.json() as Promise; -} - -export async function toolRoutes(app: FastifyInstance) { - app.get("/tools", async (req) => { - await requireAuth(req); - const registry = await getRegistry(); - return { tools: Object.values(registry) }; - }); - - app.post<{ Body: ToolInvokeRequest }>("/tools/invoke", async (req) => { - await requireAuth(req); - - const body = req.body; - const registry = await getRegistry(); - const tool = registry[body.tool]; - if (!tool) return app.httpErrors.notFound(`Unknown tool: ${body.tool}`); - - const runId = `run_${new Date().toISOString().replace(/[-:.TZ]/g, "")}_${nanoid(8)}`; - const now = new Date().toISOString(); - - const run: RunRecord = { - run_id: runId, - tenant_id: body.tenant_id, - tool: body.tool, - status: "queued", - created_at: now, - updated_at: now, - input: body.input, - artifacts: { bucket: process.env.GCS_BUCKET_ARTIFACTS ?? "", prefix: `runs/${runId}` } - }; - - await saveRun(run); - - // record input artifact - await writeArtifactText(`runs/${runId}`, "input.json", JSON.stringify(body, null, 2)); - - // execute (sync for v1; v2: push to Cloud Tasks / Workflows) - try { - run.status = "running"; - run.updated_at = new Date().toISOString(); - await saveRun(run); - - if (body.dry_run) { - run.status = "succeeded"; - run.output = { dry_run: true }; - run.updated_at = new Date().toISOString(); - await saveRun(run); - await writeArtifactText(`runs/${runId}`, "output.json", JSON.stringify(run.output, null, 2)); - return { run_id: runId, status: run.status }; - } - - const execUrl = `${tool.executor.url}${tool.executor.path}`; - const output = await postJson(execUrl, { - run_id: runId, - tenant_id: body.tenant_id, - workspace_id: body.workspace_id, - input: body.input - }); - - run.status = "succeeded"; - run.output = output; - run.updated_at = new Date().toISOString(); - await saveRun(run); - await writeArtifactText(`runs/${runId}`, "output.json", JSON.stringify(output, null, 2)); - - return { run_id: runId, status: run.status }; - } catch (e: any) { - run.status = "failed"; - run.error = { message: e?.message ?? "Unknown error" }; - run.updated_at = new Date().toISOString(); - await saveRun(run); - await writeArtifactText(`runs/${runId}`, "error.json", JSON.stringify(run.error, null, 2)); - return { run_id: runId, status: run.status }; - } - }); -} - -backend/control-plane/src/routes/runs.ts -import type { FastifyInstance } from "fastify"; -import { requireAuth } from "../auth.js"; -import { getRun } from "../storage/firestore.js"; - -export async function runRoutes(app: FastifyInstance) { - app.get("/runs/:run_id", async (req) => { - await requireAuth(req); - // @ts-expect-error fastify param typing - const runId = req.params.run_id as string; - const run = await getRun(runId); - if (!run) return app.httpErrors.notFound("Run not found"); - return run; - }); - - // V1: logs are stored as artifacts in GCS; IDE can fetch by signed URL later - app.get("/runs/:run_id/logs", async (req) => { - await requireAuth(req); - // stub - return { note: "V1: logs are in GCS artifacts under runs//" }; - }); -} - -backend/control-plane/src/index.ts -import Fastify from "fastify"; -import cors from "@fastify/cors"; -import helmet from "@fastify/helmet"; -import rateLimit from "@fastify/rate-limit"; -import { config } from "./config.js"; -import { healthRoutes } from "./routes/health.js"; -import { toolRoutes } from "./routes/tools.js"; -import { runRoutes } from "./routes/runs.js"; - -const app = Fastify({ logger: true }); - -await app.register(cors, { origin: true }); -await app.register(helmet); -await app.register(rateLimit, { max: 300, timeWindow: "1 minute" }); - -await app.register(healthRoutes); -await app.register(toolRoutes); -await app.register(runRoutes); - -app.listen({ port: config.port, host: "0.0.0.0" }).catch((err) => { - app.log.error(err); - process.exit(1); -}); - -2) Generate Tool Registry schema - -You want two things: - -A human-editable YAML (source of truth) - -A JSON Schema to validate tool definitions - -contracts/tool-registry.yaml (example) -version: 1 -tools: - cloudrun.deploy_service: - description: Deploy a Cloud Run service via Cloud Build. - risk: medium - executor: - kind: http - url: https://deploy-executor-xxxxx.a.run.app - path: /execute/deploy - inputSchema: - type: object - required: [service_name, repo, ref, env] - properties: - service_name: { type: string } - repo: { type: string, description: "Git repo URL" } - ref: { type: string, description: "Branch, tag, or commit SHA" } - env: { type: string, enum: ["dev", "staging", "prod"] } - outputSchema: - type: object - properties: - service_url: { type: string } - revision: { type: string } - - analytics.get_funnel_summary: - description: Return funnel metrics for a tenant and time window. - risk: low - executor: - kind: http - url: https://analytics-executor-xxxxx.a.run.app - path: /execute/funnel - inputSchema: - type: object - required: [range_days] - properties: - range_days: { type: integer, minimum: 1, maximum: 365 } - segment: { type: object } - -contracts/tool-registry.schema.json -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://productos.dev/schemas/tool-registry.schema.json", - "type": "object", - "required": ["version", "tools"], - "properties": { - "version": { "type": "integer", "minimum": 1 }, - "tools": { - "type": "object", - "additionalProperties": { "$ref": "#/$defs/ToolDef" } - } - }, - "$defs": { - "ToolDef": { - "type": "object", - "required": ["description", "risk", "executor", "inputSchema"], - "properties": { - "description": { "type": "string" }, - "risk": { "type": "string", "enum": ["low", "medium", "high"] }, - "executor": { "$ref": "#/$defs/Executor" }, - "inputSchema": { "type": "object" }, - "outputSchema": { "type": "object" } - }, - "additionalProperties": false - }, - "Executor": { - "type": "object", - "required": ["kind", "url", "path"], - "properties": { - "kind": { "type": "string", "enum": ["http"] }, - "url": { "type": "string" }, - "path": { "type": "string" } - }, - "additionalProperties": false - } - } -} - -3) Generate VSCodium extension skeleton -Folder layout -client-ide/extensions/gcp-productos/ - package.json - tsconfig.json - src/extension.ts - src/api.ts - src/ui.ts - media/icon.png (optional) - -client-ide/extensions/gcp-productos/package.json -{ - "name": "gcp-productos", - "displayName": "GCP Product OS", - "description": "Product-centric panels (Code, Marketing, Analytics, Growth...) and backend tool invocation.", - "version": "0.0.1", - "publisher": "productos", - "engines": { "vscode": "^1.90.0" }, - "categories": ["Other"], - "activationEvents": ["onStartupFinished"], - "main": "./dist/extension.js", - "contributes": { - "commands": [ - { "command": "productos.configure", "title": "Product OS: Configure Backend" }, - { "command": "productos.tools.list", "title": "Product OS: List Tools" }, - { "command": "productos.tools.invoke", "title": "Product OS: Invoke Tool" }, - { "command": "productos.runs.open", "title": "Product OS: Open Run" } - ], - "configuration": { - "title": "Product OS", - "properties": { - "productos.backendUrl": { - "type": "string", - "default": "http://localhost:8080", - "description": "Control Plane API base URL" - }, - "productos.tenantId": { - "type": "string", - "default": "t_dev", - "description": "Tenant ID for tool calls" - } - } - } - }, - "scripts": { - "build": "tsc -p tsconfig.json", - "watch": "tsc -w -p tsconfig.json" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "@types/vscode": "^1.90.0", - "typescript": "^5.5.4" - } -} - -client-ide/extensions/gcp-productos/tsconfig.json -{ - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true - } -} - -client-ide/extensions/gcp-productos/src/api.ts -import * as vscode from "vscode"; - -function cfg(key: string): T { - return vscode.workspace.getConfiguration("productos").get(key)!; -} - -export function backendUrl() { - return cfg("backendUrl"); -} - -export function tenantId() { - return cfg("tenantId"); -} - -export async function listTools(): Promise { - const res = await fetch(`${backendUrl()}/tools`); - if (!res.ok) throw new Error(await res.text()); - const json = await res.json(); - return json.tools ?? []; -} - -export async function invokeTool(tool: string, input: any) { - const res = await fetch(`${backendUrl()}/tools/invoke`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - tool, - tenant_id: tenantId(), - input - }) - }); - if (!res.ok) throw new Error(await res.text()); - return res.json(); -} - -export async function getRun(runId: string) { - const res = await fetch(`${backendUrl()}/runs/${runId}`); - if (!res.ok) throw new Error(await res.text()); - return res.json(); -} - -client-ide/extensions/gcp-productos/src/ui.ts -import * as vscode from "vscode"; -import { getRun } from "./api"; - -export async function showJson(title: string, obj: any) { - const doc = await vscode.workspace.openTextDocument({ - content: JSON.stringify(obj, null, 2), - language: "json" - }); - await vscode.window.showTextDocument(doc, { preview: false }); - vscode.window.setStatusBarMessage(title, 3000); -} - -export async function openRun(runId: string) { - const run = await getRun(runId); - await showJson(`Run ${runId}`, run); -} - -client-ide/extensions/gcp-productos/src/extension.ts -import * as vscode from "vscode"; -import { invokeTool, listTools } from "./api"; -import { openRun, showJson } from "./ui"; - -export function activate(context: vscode.ExtensionContext) { - context.subscriptions.push( - vscode.commands.registerCommand("productos.configure", async () => { - const backendUrl = await vscode.window.showInputBox({ prompt: "Control Plane backend URL" }); - if (!backendUrl) return; - await vscode.workspace.getConfiguration("productos").update("backendUrl", backendUrl, vscode.ConfigurationTarget.Global); - vscode.window.showInformationMessage(`Product OS backend set: ${backendUrl}`); - }) - ); - - context.subscriptions.push( - vscode.commands.registerCommand("productos.tools.list", async () => { - const tools = await listTools(); - await showJson("Tools", tools); - }) - ); - - context.subscriptions.push( - vscode.commands.registerCommand("productos.tools.invoke", async () => { - const tools = await listTools(); - const pick = await vscode.window.showQuickPick( - tools.map((t: any) => ({ label: t.name, description: t.description })), - { placeHolder: "Select a tool to invoke" } - ); - if (!pick) return; - - const inputText = await vscode.window.showInputBox({ - prompt: "Tool input JSON", - value: "{}" - }); - if (!inputText) return; - - const input = JSON.parse(inputText); - const result = await invokeTool(pick.label, input); - - await showJson("Invoke Result", result); - - if (result?.run_id) { - const open = await vscode.window.showInformationMessage(`Run started: ${result.run_id}`, "Open Run"); - if (open === "Open Run") await openRun(result.run_id); - } - }) - ); - - context.subscriptions.push( - vscode.commands.registerCommand("productos.runs.open", async () => { - const runId = await vscode.window.showInputBox({ prompt: "Run ID" }); - if (!runId) return; - await openRun(runId); - }) - ); -} - -export function deactivate() {} - -4) Generate Terraform base - -This is a minimal hub-style baseline: - -GCS bucket for artifacts - -Firestore (Native) for runs/tools - -Cloud Run service for Control Plane - -Service accounts + IAM - -Placeholders for executor services - -Folder layout -infra/terraform/ - providers.tf - variables.tf - outputs.tf - main.tf - iam.tf - -infra/terraform/providers.tf -terraform { - required_version = ">= 1.5.0" - required_providers { - google = { - source = "hashicorp/google" - version = "~> 5.30" - } - } -} - -provider "google" { - project = var.project_id - region = var.region -} - -infra/terraform/variables.tf -variable "project_id" { type = string } -variable "region" { type = string default = "us-central1" } -variable "artifact_bucket_name" { type = string } -variable "control_plane_image" { - type = string - description = "Container image URI for control-plane (Artifact Registry)." -} - -infra/terraform/main.tf -resource "google_storage_bucket" "artifacts" { - name = var.artifact_bucket_name - location = var.region - uniform_bucket_level_access = true - versioning { enabled = true } -} - -# Firestore (Native mode) – requires enabling in console once per project (or via API depending on org policy). -resource "google_firestore_database" "default" { - name = "(default)" - location_id = var.region - type = "FIRESTORE_NATIVE" -} - -resource "google_service_account" "control_plane_sa" { - account_id = "sa-control-plane" - display_name = "Product OS Control Plane" -} - -resource "google_cloud_run_v2_service" "control_plane" { - name = "control-plane" - location = var.region - - template { - service_account = google_service_account.control_plane_sa.email - - containers { - image = var.control_plane_image - env { - name = "GCP_PROJECT_ID" - value = var.project_id - } - env { - name = "GCS_BUCKET_ARTIFACTS" - value = google_storage_bucket.artifacts.name - } - env { - name = "AUTH_MODE" - value = "dev" - } - } - } -} - -# Public access optional; prefer IAM auth in production. -resource "google_cloud_run_v2_service_iam_member" "control_plane_public" { - name = google_cloud_run_v2_service.control_plane.name - location = var.region - role = "roles/run.invoker" - member = "allUsers" -} - -infra/terraform/iam.tf -# Allow control-plane to write artifacts in GCS -resource "google_storage_bucket_iam_member" "control_plane_bucket_writer" { - bucket = google_storage_bucket.artifacts.name - role = "roles/storage.objectAdmin" - member = "serviceAccount:${google_service_account.control_plane_sa.email}" -} - -# Firestore access for run/tool metadata -resource "google_project_iam_member" "control_plane_firestore" { - project = var.project_id - role = "roles/datastore.user" - member = "serviceAccount:${google_service_account.control_plane_sa.email}" -} - -# Placeholder: executor services will each have their own service accounts. -# Control-plane should be granted roles/run.invoker on each executor service once created. - -infra/terraform/outputs.tf -output "control_plane_url" { - value = google_cloud_run_v2_service.control_plane.uri -} - -output "artifact_bucket" { - value = google_storage_bucket.artifacts.name -} \ No newline at end of file diff --git a/AGENT_EXECUTION_ARCHITECTURE.md b/AGENT_EXECUTION_ARCHITECTURE.md index 812eb30..7f1dfe5 100644 --- a/AGENT_EXECUTION_ARCHITECTURE.md +++ b/AGENT_EXECUTION_ARCHITECTURE.md @@ -89,7 +89,7 @@ This is the difference between a founder who has a COO (talks to one person who └─────────────┘ └─────────────┘ └─────────────┘ ↓ ↓ ↓ ┌─────────────────────────────────────────────────────────────────────┐ -│ EXECUTION RUNTIME (Theia + vibn-agent-runner) │ +│ EXECUTION RUNTIME (vibn-agent-runner) │ │ │ │ - Persistent dev environment: Node, Python, npm, git │ │ - Agent executes commands, writes files, runs builds │ @@ -278,16 +278,6 @@ If yes → delegates to Code Advisor │ - Auto-commits to Gitea on completion │ │ - Triggers Coolify redeploy automatically │ │ - Session persists even if browser tab is closed │ -└──────────────────────┬────────────────────────────────────────────┘ - │ - ▼ -┌───────────────────────────────────────────────────────────────────┐ -│ Theia container (persistent dev environment) │ -│ │ -│ - Node.js, Python, npm, git — full dev toolchain │ -│ - sync-server.js (port 3001): git pull trigger from agent │ -│ - startup.sh: clones project repo from Gitea on boot │ -│ - PTY available for Terminal tab (Phase 5) │ └──────────────────────┬────────────────────────────────────────────┘ │ git push (auto on completion) ▼ @@ -420,15 +410,12 @@ CREATE TABLE advisor_memory ( | Auto-commit + push to Gitea on completion | ✅ | | Gitea webhook triggers Coolify auto-deploy | ✅ | | Browse tab shows latest committed files | ✅ | -| "Open in Theia →" links to theia.vibnai.com | ✅ | -| Theia sync-server running on port 3001 | ✅ | | Project tabs (Atlas/PRD/Build/Growth/Assist/Analytics) in sidebar | ✅ | ### In Progress | Step | Status | |------|--------| -| Agent `execute_command` routes through Theia container | 🔶 Phase 2 | | Assist COO + specialist advisor architecture | 🔶 Designing | ### Not Yet Started @@ -444,7 +431,7 @@ CREATE TABLE advisor_memory ( | Proactive monitoring (anomaly detection, briefings) | ⬜ | | Parallel task execution | ⬜ | | WebSocket streaming (replace polling) | ⬜ | -| Terminal tab (xterm.js → Theia PTY) | ⬜ | +| Terminal tab (xterm.js → live container PTY) | ⬜ | --- @@ -461,11 +448,11 @@ CREATE TABLE advisor_memory ( - [x] Context-aware task input (locked while running) - [x] Project tabs moved to sidebar (Atlas/PRD/Build/Growth/Assist/Analytics) -### Phase 2 — Execution in Theia 🔶 In Progress -- [x] Theia container with sync-server.js on port 3001 -- [x] startup.sh clones/pulls project repo on boot -- [ ] Agent `execute_command` routes through Theia (docker exec or HTTP) -- [ ] "Open in Theia →" reflects live agent workspace +### Phase 2 — Per-project Sandboxed Workspaces +- [ ] Per-project ephemeral container (cold tier — wakes on demand) +- [ ] Agent `execute_command` routes through the project workspace container +- [ ] Persistent volume per project for caches / installed deps +- [ ] In-browser file viewer reflects live agent workspace ### Phase 3 — Assist COO + Specialist Advisors - [ ] `advisor_conversations` + `advisor_memory` DB tables + API @@ -488,7 +475,7 @@ CREATE TABLE advisor_memory ( - [ ] WebSocket replaces polling for live output - [ ] Browser reconnect: full log replay from Postgres + live tail - [ ] Background notifications (in-app + email) on completion/failure -- [ ] Terminal tab: xterm.js connected to Theia PTY +- [ ] Terminal tab: xterm.js connected to project workspace PTY ### Phase 6 — Proactive Intelligence - [ ] Assist monitors for anomalies and surfaces them without being asked @@ -519,8 +506,5 @@ Once a specialist Advisor has produced a clear, structured plan, decomposing it **Why auto-commit by default?** The target user is a non-technical founder. Requiring approval on every task creates friction and undermines the "describe it, it ships" value proposition. Gitea + Coolify already provide a rollback path if something goes wrong. -**Why Theia as the runtime?** -Theia provides a persistent, pre-configured dev environment — Node, Python, git, PTY — with no cold-start cost. We use it as infrastructure; the user never sees the Theia UI. VIBN is the product. - **Why store everything in Postgres?** Browser sessions end. Postgres is the source of truth. Every conversation turn, every memory item, every execution step, every outcome is written immediately. The WebSocket stream (Phase 5) is a convenience layer on top of the database, not a replacement. diff --git a/AGENT_TELEMETRY_STREAMING_PROJECT.md b/AGENT_TELEMETRY_STREAMING_PROJECT.md new file mode 100644 index 0000000..79a1dee --- /dev/null +++ b/AGENT_TELEMETRY_STREAMING_PROJECT.md @@ -0,0 +1,292 @@ +# Agent telemetry & live execution stream — project spec + +This document captures **concrete product and engineering additions** discussed for Vibn: moving from **poll-based session updates** and **in-memory jobs** to a **durable, ordered, push-friendly execution timeline**—the web equivalent of a terminal agent’s clarity (step-by-step visibility, tool boundaries, failures, and later multi-agent signals). + +--- + +## 1. Why this exists + +### Current behavior (baseline) + +| Surface | How progress reaches the user | Limits | +|--------|------------------------------|--------| +| **Agent sessions** (`agent_sessions`) | Runner `PATCH`es `output`, `status`, `changed_files` to Next; UI **polls** `GET …/agent/sessions/[id]`. | Latency, reconnect story, no single ordered stream; rich semantics encoded only in `text`. | +| **Jobs** (`/api/agent/run`, `/api/jobs/:id`) | In-memory `job-store` (`progress`, `toolCalls[]`); UI polls job endpoint. | Lost on restart; not shared across runner replicas; not unified with session UI. | +| **Orchestrator / Atlas chat** | Request/response to runner; advisor path may be remote URL. | No execution timeline for “long COO run” in-product unless you add the same event layer. | + +### Product intent + +- **Trust during long runs**: users see *what* happened, *when*, and *whether something was blocked*—not only a final status. +- **Differentiation**: “Ink-like” clarity in the browser—structured steps, not a blob of logs. +- **Foundation for multi-agent**: handoffs, child work, and safety events need a **common event pipe**, not ad-hoc strings. + +--- + +## 2. Goals + +1. **Append-only execution events** with **monotonic ordering** (per session or per job), suitable for replay after refresh. +2. **Server-push to the client** (recommend **SSE** first; WebSocket if you need bi-directional on the same channel). +3. **Persistence** so reconnect, refresh, and horizontal scaling do not lose history. +4. **Single conceptual model** (`AgentEvent`) usable by: + - Build → **Agent** tab (sessions), + - **Job** flows (create/analyze-style), + - optionally **orchestrator** long runs later. +5. **Backward compatibility** during rollout: existing `PATCH` + `output` can remain as a fallback or be fed from the same emitter. + +### Non-goals (for v1) + +- Full **OpenTelemetry** export (optional later). +- **Real-time collaborative** multi-user cursors on the same session. +- Merging **claude-code-fork**—this spec is **API + UI + persistence** only. + +--- + +## 3. Concept: `AgentEvent` + +### Core shape (suggested) + +```ts +type AgentEvent = { + seq: number; // monotonic per stream (session_id or job_id) + ts: string; // ISO-8601 + runId: string; // session UUID or job id — ties events to a run + runKind: 'session' | 'job'; + phase: 'queued' | 'running' | 'completed' | 'failed' | 'stopped'; + + type: AgentEventType; + payload: Record; // type-specific +}; + +type AgentEventType = + | 'run.started' + | 'run.phase' // e.g. planning, executing, committing + | 'llm.turn.start' + | 'llm.turn.end' + | 'tool.start' + | 'tool.end' + | 'tool.output' // chunked stdout/stderr if needed + | 'safety.block' // policy / protected path / command denied + | 'file.changed' // maps to today’s changed_files semantics + | 'git.commit' + | 'deploy.triggered' + | 'deploy.status' + | 'error' + | 'run.completed' + | 'handoff' // v2: parent → child agent + | 'child_job.started' // v2: linked run id + ; +``` + +### Mapping from today’s session `outputLine` + +| Today (`outputLine.type`) | Suggested event(s) | +|---------------------------|--------------------| +| `step` / `info` | `run.phase` or `llm.turn.*` with summary in `payload.message` | +| `stdout` / `stderr` | `tool.output` or dedicated stream events | +| `error` | `error` + optional `safety.block` if policy-driven | +| `done` | `run.completed` | + +Keep **human-readable `message`** on events for UI defaults; add **structured fields** (`tool`, `argsSummary`, `durationMs`) for timeline rendering and filters. + +--- + +## 4. Architecture (high level) + +```mermaid +flowchart LR + subgraph runner [vibn-agent-runner] + RA[runSessionAgent / runAgent] + EMIT[emitAgentEvent] + end + subgraph api [vibn-frontend Next.js] + ING[POST internal ingest or PATCH extend] + DB[(Postgres agent_events)] + SSE[SSE GET /api/.../stream] + end + subgraph browser [Browser] + UI[Timeline + live log] + end + RA --> EMIT + EMIT -->|HTTPS + secret or mTLS| ING + ING --> DB + UI -->|EventSource| SSE + SSE --> DB +``` + +**Principles** + +- **Runner remains stateless** regarding “truth”: it emits events; **Next + DB** are the source of truth for the UI (matches today’s session model). +- Alternatively, runner could expose **SSE directly**—usually worse for **auth**, **CORS**, and **one domain** for the product. Prefer **Next as SSE endpoint** reading from DB. + +--- + +## 5. Backend: `vibn-agent-runner` + +### 5.1 Emit from execution paths + +| Location | Action | +|----------|--------| +| `agent-session-runner.ts` | Replace or supplement `patchSession` output-only updates with **`emitAgentEvent`** each turn / tool / error. | +| `runAgent` / tool loop (`executeTool`) | Same emitter for **job** runs. | +| `server.ts` `/agent/execute` | Emit `run.started` after 202; `run.completed` / `error` on exit. | +| Security / blocked tools (`security.ts` or equivalent) | Emit `safety.block` with reason code (no secrets in payload). | + +### 5.2 Transport runner → Next + +**Option A (recommended):** extend existing **PATCH** or add **`POST /api/internal/agent-events`** (or per-session batch append): + +- Headers: `x-agent-runner-secret` (same as today’s PATCH). +- Body: single event or small batch `{ events: AgentEvent[] }` with server-assigned `seq` to avoid races. + +**Option B:** Runner writes to **Redis/Postgres** directly—couples runner to DB credentials; only do if you already run runner inside the same trust zone with DB URL. + +### 5.3 Jobs store + +- **Short term:** continue in-memory for job metadata; **persist events** to Postgres keyed by `jobId`. +- **Medium term:** optional **Redis** for job status + pub/sub to Next for low-latency SSE fanout (only if DB polling becomes a bottleneck). + +--- + +## 6. Backend: `vibn-frontend` (Next.js) + +### 6.1 Persistence + +**New table (example): `agent_run_events`** + +| Column | Notes | +|--------|--------| +| `id` | UUID | +| `run_id` | Session id or job id (text) | +| `run_kind` | `'session' \| 'job'` | +| `seq` | BIGSERIAL or per-run sequence enforced with unique constraint `(run_id, seq)` | +| `project_id` | Nullable for jobs if not scoped | +| `event` | JSONB — full `AgentEvent` or `{ type, ts, payload }` | +| `created_at` | default now() | + +Index: `(run_id, seq)` for range queries (`WHERE run_id = $1 AND seq > $lastSeen`). + +**Optional:** migrate legacy `agent_sessions.output` to be **derived** (last N lines for email export) or **dual-write** during transition. + +### 6.2 SSE route (example contract) + +- **`GET /api/projects/[projectId]/agent/sessions/[sessionId]/events/stream`** + - Auth: session cookie / same as GET session (user must own project). + - Query: `?afterSeq=123` for replay. + - Response: `text/event-stream`; each message: `data: {JSON}\n\n`. + - Heartbeat comments every ~15–30s to keep proxies alive. + +For **jobs** (if not project-scoped): `GET /api/jobs/[jobId]/events/stream` with appropriate auth. + +### 6.3 Ingest route (runner-only) + +- **`POST /api/internal/agent-events`** (or nested under project/session as you prefer). +- Validates `x-agent-runner-secret`. +- Inserts rows with **server-generated `seq`** (transaction per run or advisory lock per `run_id`). + +--- + +## 7. Frontend (product UI) + +### 7.1 Agent tab — timeline + +- **EventSource** (SSE) subscription when session is `running`; on load, **fetch historical** events (`GET …/events?afterSeq=0` or SSE from 0). +- **Timeline components**: + - Group by `llm.turn` / `tool.start`–`tool.end`. + - Expandable tool args (sanitized). + - Distinct styling for `safety.block` and `error`. +- **Reconnect**: on `EventSource` error, reopen with `lastSeq` from last received event. + +### 7.2 Jobs / analyze flows + +- Same timeline component keyed by `jobId` if you surface those runs in UI. +- Unifies mental model: “every run has a stream.” + +### 7.3 Deprecate slow polling + +- Reduce `GET …/agent/sessions/[id]` poll interval when SSE connected; keep **single poll** for `status` / `changed_files` if those stay on session row only, or **also** emit `file.changed` events and drive UI from stream + one final consistency read. + +--- + +## 8. Security & privacy + +- **Never** put tokens, env values, or full file contents in events by default; use **truncation** and **hashes** where needed. +- **`safety.block`**: log reason **code** + user-safe message; align with `security.ts` behavior. +- **Rate limits** on ingest endpoint (per `run_id` / per IP) to avoid abuse if misconfigured. + +--- + +## 9. Environment variables + +| Variable | Where | Purpose | +|----------|--------|---------| +| `AGENT_RUNNER_SECRET` | Runner + Next | Ingest / extended PATCH auth | +| `VIBN_API_URL` | Runner | Base URL for callbacks | +| `AGENT_RUNNER_URL` | Next | Start runs (unchanged) | + +Add if needed: + +| Variable | Purpose | +|----------|---------| +| `AGENT_EVENTS_INGEST_PATH` | Optional override for ingest URL | +| `SSE_MAX_BUFFER` | Cap replay batch size | + +--- + +## 10. Phased roadmap (suggested) + +### Phase 1 — Foundation + +- [ ] Define `AgentEvent` TypeScript types in a **shared package** or duplicated minimal types in runner + frontend. +- [ ] Create `agent_run_events` (or equivalent) + migration. +- [ ] Implement **ingest** endpoint; wire **runner session path** to emit core events: `run.started`, `tool.start` / `tool.end`, `error`, `run.completed`, `file.changed`. +- [ ] **Dual-write**: keep existing `PATCH` `outputLine` so nothing breaks. + +### Phase 2 — Push + +- [ ] SSE route + **EventSource** in Agent tab. +- [ ] Backfill UI from DB on mount; then live tail. +- [ ] Lower or gate polling on `GET` session. + +### Phase 3 — Jobs + durability + +- [ ] Emit same events from **job** execution path; persist by `jobId`. +- [ ] Optional: replace in-memory job list with DB for **multi-instance** runner (later). + +### Phase 4 — Rich semantics + +- [ ] `safety.block` from policy layer. +- [ ] `deploy.*` events if Coolify integration is user-visible. +- [ ] **Multi-agent**: `handoff`, `child_job.*` with links in payload. + +--- + +## 11. Success metrics + +- Time-to-first-visible-step after **Run** < **1s** p95 (SSE). +- After hard refresh mid-run, user sees **consistent history** (no duplicate seq, no gaps if you guarantee at-least-once ingest with idempotency keys later). +- Support tickets / confusion drops on “what is the agent doing?” (qualitative). + +--- + +## 12. Related code (repo anchors) + +Use these when implementing: + +- Runner session loop + PATCH bridge: `vibn-agent-runner/src/agent-session-runner.ts` +- Runner HTTP: `vibn-agent-runner/src/server.ts` (`/agent/execute`, `/agent/stop`, `/agent/approve`, `/api/agent/run`, `/api/jobs/:id`) +- In-memory jobs: `vibn-agent-runner/src/job-store.ts` +- Next session API + runner callback: `vibn-frontend/app/api/projects/[projectId]/agent/sessions/[sessionId]/route.ts` +- Session create + fire-and-forget execute: `vibn-frontend/app/api/projects/[projectId]/agent/sessions/route.ts` + +--- + +## 13. Open decisions + +1. **Single table** for sessions + jobs vs **two tables** (simpler queries vs flexibility). +2. **Seq generation**: DB sequence per `run_id` vs global monotonic with `(run_id, seq)` composite only in app logic. +3. **Idempotency**: runner retries may duplicate events—use **`event_id` UUID** from runner for dedupe on ingest. +4. **Orchestrator chat**: treat as v2 unless you need a **COO run** timeline immediately. + +--- + +*Document version: 1.0 — aligned with discussion of runner ↔ frontend telemetry, SSE-first delivery, Postgres persistence, and future multi-agent event types.* diff --git a/AI_CAPABILITIES.md b/AI_CAPABILITIES.md new file mode 100644 index 0000000..f0c9c2f --- /dev/null +++ b/AI_CAPABILITIES.md @@ -0,0 +1,584 @@ +# Vibn AI Capabilities + +> The full set of actions an AI agent can take on behalf of a Vibn workspace, +> along with the REST endpoints, MCP tools, and safety rails that back them. +> +> **Audience:** agent authors, Cursor rule writers, MCP tool designers, and +> anyone building on the Vibn control plane. +> +> **Scope:** everything an agent sees through `https://vibnai.com/api/*` and +> the `/api/mcp` bridge. No Firestore, no internal agent orchestration — +> just the tenant-safe capability surface. + +--- + +## 1. Mental model + +Every capability in this document operates on a single **workspace**. A +workspace is Vibn's tenant boundary and maps 1:1 to: + +| Vibn concept | External identity | Example (`mark`) | +|---|---|---| +| Workspace | `vibn_workspaces.slug` | `mark` | +| Gitea org | `gitea_org` | `vibn-mark` | +| Gitea bot user | `gitea_bot_username` | `mark-bot` | +| SSH deploy keypair | `coolify_private_key_uuid` + `gitea_bot_ssh_key_id` | registered on both sides | +| Coolify project | `coolify_project_uuid` | `vibn-ws-mark` | +| Coolify environment | `coolify_environment_name` | `production` | +| Domain namespace | `*.{slug}.vibnai.com` | `*.mark.vibnai.com` | +| AI token | `vibn_sk_…` | one per agent/device | + +A single agent token can only act on the workspace it was minted for. Cross- +workspace access is structurally impossible — enforced in +[`lib/coolify.ts`](./vibn-frontend/lib/coolify.ts) by matching every Coolify +resource's `environment_id` against the workspace's project environments +(`ensureResourceInProject`). + +### The three views + +All capabilities roll up into three user-facing surfaces: + +- **Code** — every Gitea repo under `vibn-{slug}/`. +- **Live** — every Coolify app/database/service in `vibn-ws-{slug}`, each + reachable under `*.{slug}.vibnai.com`. +- **IDE** — Browser-based agent workspace sessions (outside the scope of this doc). + +--- + +## 2. Authentication + +Every agent-facing endpoint accepts **either**: + +- `Authorization: Bearer vibn_sk_` — a workspace-scoped API key + minted in the settings panel. Stored as a sha256 hash server-side; the + plaintext is shown exactly once on creation. Can be revoked at any time. +- A NextAuth session cookie — used for the dashboard UI and for browser + debugging. Not suitable for long-running agents. + +Helper: [`requireWorkspacePrincipal()`](./vibn-frontend/lib/auth/workspace-auth.ts) +resolves either to a `WorkspacePrincipal { workspace, user?, source }`. + +**403 on a tenant mismatch means:** the token is valid, but the resource +belongs to another workspace. The agent should stop and ask the user. + +--- + +## 3. MCP surface + +The MCP bridge lives at `POST https://vibnai.com/api/mcp`. It takes +JSON-over-HTTP bodies shaped like: + +```json +{ "tool": "", "params": { /* tool-specific */ } } +``` + +The Cursor / Claude Desktop config block is auto-generated in the settings +panel and looks like: + +```json +{ + "mcpServers": { + "vibn-mark": { + "url": "https://vibnai.com/api/mcp", + "headers": { "Authorization": "Bearer vibn_sk_…" } + } + } +} +``` + +`GET /api/mcp` returns a self-description with the current tool list. +Version: **2.1.0**. + +### 3.1 Workspace & identity tools + +| Tool | Purpose | Params | +|---|---|---| +| `workspace.describe` | Returns slug, Coolify project uuid, Gitea org, provision status. | — | +| `gitea.credentials` | Returns the bot's username, PAT, clone URL template, and SSH remote template. Use this for every `git clone`/push — never other credentials. | — | + +### 3.2 Project tools + +| Tool | Purpose | Params | +|---|---|---| +| `projects.list` | Lists Vibn projects (PRDs, imports, etc.) in the workspace. | — | +| `projects.get` | Single project details. | `{ projectId }` | + +### 3.3 Application tools + +| Tool | Purpose | Params | +|---|---|---| +| `apps.list` | All Coolify apps in the workspace. | — | +| `apps.get` | Single app details (status, fqdn, domains, git info). | `{ uuid }` | +| `apps.create` | Create a Coolify app from a Gitea repo in the workspace's org, pinned to the workspace's SSH deploy key. Auto-domain `{name}.{slug}.vibnai.com`. | `{ repo, branch?, name?, ports?, buildPack?, domain?, envs?, instantDeploy? }` | +| `apps.update` | PATCH a whitelisted set of fields (name, description, git branch, ports, build commands, base directory, Dockerfile location…). | `{ uuid, patch }` | +| `apps.delete` | Destroy the app. Volumes kept by default. | `{ uuid, confirm }` — `confirm` must equal the app's exact name | +| `apps.deploy` | Trigger a new deployment. | `{ uuid, force? }` | +| `apps.deployments` | List recent deployments + status. | `{ uuid }` | +| `apps.domains.list` | Current domain set. | `{ uuid }` | +| `apps.domains.set` | Replace the domain set. All entries must end with `.{slug}.vibnai.com`. | `{ uuid, domains: string[] }` | +| `apps.envs.list` | List env vars. Values returned are redacted for `shown-once` secrets. | `{ uuid }` | +| `apps.envs.upsert` | Create or update an env var. | `{ uuid, key, value, isBuildTime?, isMultiline?, isLiteral?, isShownOnce? }` | +| `apps.envs.delete` | Delete an env var. | `{ uuid, key }` | + +### 3.4 Database tools + +| Tool | Purpose | Params | +|---|---|---| +| `databases.list` | All databases in the workspace, across all flavors. | — | +| `databases.create` | Provision a database. Supported `type`: `postgresql`, `mysql`, `mariadb`, `mongodb`, `redis`, `keydb`, `dragonfly`, `clickhouse`. | `{ type, name?, isPublic?, publicPort?, image?, credentials?, limits? }` | +| `databases.get` | Details + internal connection URL. | `{ uuid }` | +| `databases.update` | PATCH name, public visibility, image, limits. | `{ uuid, patch }` | +| `databases.delete` | Destroy the database. Volumes kept by default. | `{ uuid, confirm }` — `confirm` must equal the db's exact name | + +### 3.5 Auth provider tools + +Authentication is a first-class capability. An agent cannot spin up arbitrary +Coolify services — only vetted auth providers from an allowlist. + +| Tool | Purpose | Params | +|---|---|---| +| `auth.list` | Auth providers currently deployed in the workspace (classified by Coolify's `service_type`). | — | +| `auth.create` | Provision one of the allowed providers. | `{ provider, name?, description?, instantDeploy? }` | +| `auth.delete` | Destroy an auth provider. Volumes (user data) kept by default. | `{ uuid, confirm }` — `confirm` must equal the service's exact name | + +**Allowed providers** (keys passed as `provider`): + +- `pocketbase` — lightweight (SQLite) auth + data, single container. +- `authentik` — feature-rich self-hosted IDP. +- `keycloak` / `keycloak-with-postgres` — industry-standard OIDC/SAML. +- `pocket-id` / `pocket-id-with-postgresql` — passkey-first OIDC. +- `logto` — dev-first IDP. +- `supertokens-with-postgresql` — session/auth backend. + +Requesting anything outside this list returns 400 with a hint listing the +allowed ones, so the agent can self-correct. + +### 3.6 Domain tools (P5.1 — custom apex domains) + +Custom apex domains are owned end-to-end by Vibn: the registrar is OpenSRS +(Tucows), authoritative DNS is Google Cloud DNS in the Canadian project, and +domains are pinned to the workspace that registered them. All four lifecycle +steps — search, register, attach, inspect — are agent-callable. + +| Tool | Purpose | Params | +|---|---|---| +| `domains.search` | Check availability + price for one or more candidate apex domains via OpenSRS. Stateless; does not reserve anything. | `{ names: string[], period?: number }` — `names` up to 25, `period` in years (auto-bumped for quirky TLDs like `.ai` which requires 2y minimum). | +| `domains.register` | Register a domain through OpenSRS. Registers unlocked; locking happens automatically after `domains.attach` completes. Idempotent per `(workspace, domain)`. | `{ domain, period?, whoisPrivacy?, contact, nameservers?, ca?: { cprCategory, legalType } }` — `ca.*` required for `.ca`. | +| `domains.list` | List all domains owned by the workspace with their status, registrar order id, expiry, and DNS provider/zone. | — | +| `domains.get` | Full record + last 20 lifecycle events. | `{ domain }` | +| `domains.attach` | Wire a registered domain to a Coolify app (or arbitrary IP/CNAME): create Cloud DNS zone, write A/CNAME rrsets, update registrar-side nameservers, append FQDNs to the Coolify app's domain list. Idempotent; safe to retry. | `{ domain, appUuid? \| ip? \| cname?, subdomains?: string[] (default ["@","www"]), updateRegistrarNs? }` | + +**Residency note:** Cloud DNS is global anycast — configuration is not +Canadian-pinned at the storage layer. The workspace-level `dns_provider` +flag (default `cloud_dns`) will let us swap in CIRA D-Zone for strict +Canadian residency without touching the MCP surface. + +**Billing:** Every successful `domains.register` writes a `debit` row to +`vibn_billing_ledger` with the OpenSRS order id as `ref_id`. The +`vibn_domain_events` table keeps an append-only audit of every lifecycle +call (`register.attempt`, `register.success`, `register.failed`, +`attach.success`). + +**Verified end-to-end (2026-04-22)** against PROD GCP + OpenSRS sandbox + +PROD Coolify (Coolify `v4.0.0-beta.473`); see +`vibn-frontend/scripts/smoke-attach-e2e.ts`. **All 5 sub-systems green.** + +- ✓ OpenSRS register against Horizon (sandbox) returns order id, response 200. +- ✓ Cloud DNS managed zone created in `master-ai-484822` with public anycast NS. +- ✓ A records (`@`, `www`) written to the zone. +- ✓ Registrar-side nameserver update accepts Cloud DNS NS values + (trailing-dot normalization in `lib/opensrs.ts`); sandbox returns 480 + because its mock registry doesn't know real Google NS hosts, which is + expected — live mode talks to real registries that accept any resolvable NS. +- ✓ Unlock → update NS → relock fallback path verified (sandbox-recognized + nameservers return 200; the unlock/relock sequence is exercised when the + registry returns 405 lock-conflict). +- ✓ Coolify domain-list PATCH adds the apex + `www` to the application + `fqdn` column and the smoke test re-fetches it to confirm. + +> **Operational gotcha — the destination server must be proxy-enabled.** +> Coolify's `update_by_uuid` controller accepts `domains` as a comma-separated +> list and only maps it onto the model's `fqdn` column when the destination +> server's `Server::isProxyShouldRun()` returns `true`. That helper requires +> **both** `proxy.type ∈ {TRAEFIK, CADDY}` *and* `is_build_server = false`. +> If either is misconfigured the PATCH returns 200 but the field is silently +> dropped (Laravel mass-assignment ignores `domains` because it isn't in +> `$fillable`, and the controller never copies it into `fqdn`). We hit this +> on `coolify-server-mtl` (`zg4cwgc44ogc08804000gggo`), which had +> `proxy=null` and `is_build_server=true`. Fixed by: +> +> ```sql +> UPDATE servers +> SET proxy = jsonb_set(coalesce(proxy,'{}'::jsonb), '{type}', '"TRAEFIK"') +> WHERE uuid = 'zg4cwgc44ogc08804000gggo'; +> UPDATE server_settings +> SET is_build_server = false +> WHERE server_id = (SELECT id FROM servers WHERE uuid = 'zg4cwgc44ogc08804000gggo'); +> ``` +> +> followed by `docker restart coolify` to clear Laravel's in-memory config. +> Sending `fqdn` directly is **not** an alternative — the controller's +> `$allowedFields` whitelist rejects it with 422 "This field is not allowed." + +### 3.7 Agent-side stdio MCP servers (`vibn-agent-runner`) + +Separate from the control-plane MCP at `/api/mcp` (which is what external +agents call *into* Vibn), the `vibn-agent-runner` exposes its own in-house +tool surface *outward* over stdio MCP. This lets Cursor, Claude Desktop, +Goose, or any MCP-speaking client drive the same Coolify / Gitea / workspace +tooling the Coder/PM/Marketing sub-agents use internally — with the same +protected-repo and protected-app guardrails enforced centrally. + +Architecture: every tool now has three touch-points backed by one source of truth: + +``` +vibn-agent-runner/src/tools/-api.ts ← pure, config-agnostic logic + security guards +vibn-agent-runner/src/tools/.ts ← thin registerTool() wrappers for the in-process agent loop +vibn-agent-runner/src/mcp/-server.ts ← stdio MCP server for external clients +``` + +| Server | Tools | Required env | +|---|---|---| +| `vibn-coolify-mcp` | 7 — list_projects, list_applications, deploy, get_logs, list_all_apps, get_app_status, deploy_app | `COOLIFY_API_URL`, `COOLIFY_API_TOKEN` | +| `vibn-gitea-mcp` | 6 — create/list/close issues, list_repos, list_all_issues, read_repo_file | `GITEA_API_URL`, `GITEA_API_TOKEN`, `GITEA_USERNAME` | +| `vibn-workspace-mcp` | 8 — read/write/replace/list/find/search_code, execute_command, git_commit_and_push | `WORKSPACE_ROOT` (+ Gitea creds for git push) | +| `vibn-platform-mcp` | 7 — save_memory, list_memory, list_skills, get_skill, finalize_prd, get_prd, web_search | `SESSION_KEY` (optional), Gitea creds (for skills) | +| `vibn-agent-mcp` | 2 — spawn_agent, get_job_status (dispatches into the runner's HTTP API) | `AGENT_RUNNER_URL` (defaults to `http://localhost:3333`) | + +Run locally with `npm run mcp:` (or `:dev` via ts-node) in +`vibn-agent-runner/`. Smoke-test any server with +`node scripts/smoke-mcp.js `. The in-process agent loop still sees +the same 28 registered tools — no behavioral regression. + +--- + +## 4. REST surface + +Every MCP tool is also exposed as a plain HTTP endpoint under +`/api/workspaces/{slug}/…`. Agents that prefer curl-style access can use +these directly; the shape is identical to the MCP `params`. Auth is the +same bearer header. + +### 4.1 Workspace & key management + +| Method | Path | Description | +|---|---|---| +| GET | `/api/workspaces` | All workspaces the principal has access to. | +| GET | `/api/workspaces/{slug}` | Workspace details. | +| POST | `/api/workspaces/{slug}/provision` | Idempotent re-run of Gitea org + bot + SSH keypair + Coolify project setup. | +| GET | `/api/workspaces/{slug}/keys` | List API keys (metadata only). | +| POST | `/api/workspaces/{slug}/keys` | Mint a new API key. Full token returned once. | +| DELETE | `/api/workspaces/{slug}/keys/{keyId}` | Revoke a key. | +| GET | `/api/workspaces/{slug}/gitea-credentials` | Return bot username, PAT (decrypted), clone/SSH templates. | +| GET | `/api/workspaces/{slug}/bootstrap.sh` | Shell script that writes `.cursor/rules`, `.cursor/mcp.json`, `.env.local` into the cwd. | + +### 4.2 Applications + +| Method | Path | Description | +|---|---|---| +| GET | `/api/workspaces/{slug}/apps` | List apps. | +| POST | `/api/workspaces/{slug}/apps` | Create an app from a workspace repo. | +| GET | `/api/workspaces/{slug}/apps/{uuid}` | App details. | +| PATCH | `/api/workspaces/{slug}/apps/{uuid}` | Update whitelisted fields. | +| DELETE | `/api/workspaces/{slug}/apps/{uuid}?confirm=` | Destroy app. | +| POST | `/api/workspaces/{slug}/apps/{uuid}/deploy` | Trigger deploy. | +| GET | `/api/workspaces/{slug}/apps/{uuid}/deployments` | List deployments. | +| GET | `/api/workspaces/{slug}/apps/{uuid}/domains` | List domains. | +| PATCH | `/api/workspaces/{slug}/apps/{uuid}/domains` | Replace domain set. | +| GET | `/api/workspaces/{slug}/apps/{uuid}/envs` | List env vars. | +| PATCH | `/api/workspaces/{slug}/apps/{uuid}/envs` | Upsert env var(s). | +| DELETE | `/api/workspaces/{slug}/apps/{uuid}/envs?key=FOO` | Delete env var. | +| GET | `/api/workspaces/{slug}/deployments/{deploymentUuid}/logs` | Deployment logs. | + +### 4.3 Databases + +| Method | Path | Description | +|---|---|---| +| GET | `/api/workspaces/{slug}/databases` | List databases. | +| POST | `/api/workspaces/{slug}/databases` | Create a database (8 flavors). | +| GET | `/api/workspaces/{slug}/databases/{uuid}` | Database details + internal connection URL. | +| PATCH | `/api/workspaces/{slug}/databases/{uuid}` | Update fields. | +| DELETE | `/api/workspaces/{slug}/databases/{uuid}?confirm=` | Destroy database. | + +### 4.4 Auth providers + +| Method | Path | Description | +|---|---|---| +| GET | `/api/workspaces/{slug}/auth` | List deployed auth providers + the allowlist. | +| POST | `/api/workspaces/{slug}/auth` | Provision a provider from the allowlist. | +| GET | `/api/workspaces/{slug}/auth/{uuid}` | Provider details. | +| DELETE | `/api/workspaces/{slug}/auth/{uuid}?confirm=` | Destroy provider. | + +### 4.5 Domains (P5.1) + +| Method | Path | Description | +|---|---|---| +| POST | `/api/workspaces/{slug}/domains/search` | Availability + pricing for up to 25 candidate names. | +| GET | `/api/workspaces/{slug}/domains` | List workspace-owned domains. | +| POST | `/api/workspaces/{slug}/domains` | Register a domain (idempotent per `(workspace, domain)`). | +| GET | `/api/workspaces/{slug}/domains/{domain}` | Full record + last 20 events. | +| POST | `/api/workspaces/{slug}/domains/{domain}/attach` | Create Cloud DNS zone, write records, update registrar NS, wire Coolify domain list. | + +--- + +## 5. Gitea surface + +AI agents **never** talk to the root Gitea admin token. They use the +workspace's dedicated bot user. + +### 5.1 What the bot can do + +- Fully own the `vibn-{slug}` org (added as the org's owner team). +- Read/write every repo in that org via its PAT. +- Push over SSH using the workspace's ed25519 deploy key (same keypair + Coolify uses to pull code). +- What it **cannot** do: touch any other org, the root admin surface, or + Gitea's `/admin/*` endpoints. + +### 5.2 How to get the bot credentials + +```http +GET /api/workspaces/{slug}/gitea-credentials +Authorization: Bearer vibn_sk_… +``` + +Returns: + +```json +{ + "bot": { "username": "mark-bot", "token": "…" }, + "gitea": { + "apiBase": "https://git.vibnai.com/api/v1", + "host": "git.vibnai.com", + "cloneUrlTemplate": "https://mark-bot:{{token}}@git.vibnai.com/vibn-mark/{{repo}}.git", + "sshRemoteTemplate": "git@git.vibnai.com:vibn-mark/{{repo}}.git", + "webUrlTemplate": "https://git.vibnai.com/vibn-mark/{{repo}}" + }, + "workspace": { "slug": "mark", "giteaOrg": "vibn-mark" } +} +``` + +The PAT is stored **encrypted at rest** using AES-256-GCM with the +`VIBN_SECRETS_KEY` server secret; the decrypt step runs only on this endpoint. + +### 5.3 Gitea operations via the standard Gitea API + +Once the agent has `{bot.token, gitea.apiBase}`, it can call any standard +Gitea v1 endpoint as the bot, scoped to the workspace org. Common ones: + +- `POST /orgs/{org}/repos` — create a repo. +- `PATCH /repos/{org}/{repo}` — update repo settings. +- `GET /repos/{org}/{repo}/contents/{path}` — read files. +- `PUT /repos/{org}/{repo}/contents/{path}` — write files (commits). +- `POST /repos/{org}/{repo}/pulls` — open PRs. +- `POST /repos/{org}/{repo}/branches` — create branches. + +--- + +## 6. Domain policy + +Every app gets an auto-generated domain under the workspace's namespace: + +``` +{app-slug}.{workspace-slug}.vibnai.com +``` + +For example, creating an app named `my-api` in workspace `mark` yields +`my-api.mark.vibnai.com` automatically — no DNS config, no cert work, +served by Coolify's wildcard Traefik. + +### 6.1 What agents can do + +- Accept the auto-generated domain (default path). +- Replace the domain set via `PATCH /apps/{uuid}/domains`, provided every + entry ends with `.{workspace-slug}.vibnai.com`. + +### 6.2 What agents cannot do + +- Point an app at a domain outside the workspace's namespace. The server + rejects this with 403 regardless of DNS state: + + ```json + { "error": "Domain evil.com is not allowed; must end with .mark.vibnai.com", + "hint": "Use my-api.mark.vibnai.com" } + ``` + +This is enforced by `isDomainUnderWorkspace()` in +[`lib/naming.ts`](./vibn-frontend/lib/naming.ts). + +### 6.3 Custom (external) domains + +Not exposed to AI agents. A human can still add them through Coolify +directly or through a future human-gated UI. + +--- + +## 7. Safety model + +### 7.1 Tenant enforcement + +Every resource-returning helper in `lib/coolify.ts` runs through +`ensureResourceInProject()`. It: + +1. Trusts an explicit `project_uuid` on the resource if present, else +2. Fetches the project's environment ids via `GET /projects/{uuid}` and + verifies the resource's `environment_id` is in that set. + +A token for `mark` that tries to read an app in `justine`'s project returns: + +```json +{ "error": "Application does not belong to project " } +``` + +with HTTP 403. Cross-workspace enumeration and access are not just +discouraged — they fail at the helper level. + +### 7.2 Destructive operations + +Every delete endpoint requires `?confirm=`: + +``` +DELETE /apps/{uuid} → 409 "confirmation required" +DELETE /apps/{uuid}?confirm=wrong → 409 "confirmation required" +DELETE /apps/{uuid}?confirm=my-api → 200 deleted +``` + +This means an agent hallucinating a delete call cannot cost you the +resource — it must first know the exact name, which implies it just listed +or just created it. + +**Volumes are kept by default** on delete. To also remove volumes, pass +`?volumes=delete` (apps/dbs) — this is opt-in, per-call, never the default. + +### 7.3 Creation guardrails + +- Apps can only be created from repos in the workspace's Gitea org. +- Auth providers can only be created from the allowlist (see §3.5). +- Database flavors are restricted to the 8 Coolify supports. +- Env var keys must match `/^[A-Z_][A-Z0-9_]*$/` (no shell-escape tricks). + +### 7.4 Secrets handling + +- `VIBN_API_KEY` is only shown **once** on mint. Server keeps a sha256 hash. +- Gitea bot PATs are **encrypted at rest** (AES-256-GCM with + `VIBN_SECRETS_KEY`). +- The SSH private key is held by Coolify, not by Vibn; the public key is + pushed to the Gitea bot user's key list. Rotating is a re-provision. +- Agent prompts and Cursor rules include a "treat VIBN_API_KEY like a + password — never print or commit it" directive. + +--- + +## 8. Worked examples + +### 8.1 "Build me a Next.js app with a Postgres and Pocketbase auth" + +From the agent's side, using MCP: + +```json +// 1. Ensure a repo exists in the workspace org (standard Gitea API, +// using the bot PAT from gitea.credentials). +POST https://git.vibnai.com/api/v1/orgs/vibn-mark/repos +{ "name": "my-site", "private": true, "auto_init": true } + +// 2. Create the Coolify app. Auto-domain my-site.mark.vibnai.com. +{ "tool": "apps.create", + "params": { "repo": "my-site", "ports": "3000", "instantDeploy": false } } + +// 3. Provision a Postgres. +{ "tool": "databases.create", + "params": { "type": "postgresql", "name": "app-db" } } +// → returns { internalUrl: "postgres://…@:5432/postgres" } + +// 4. Wire the db URL into the app as an env var. +{ "tool": "apps.envs.upsert", + "params": { "uuid": "", "key": "DATABASE_URL", + "value": "" } } + +// 5. Deploy Pocketbase as the auth layer. +{ "tool": "auth.create", + "params": { "provider": "pocketbase", "name": "auth" } } + +// 6. First real deploy. +{ "tool": "apps.deploy", "params": { "uuid": "" } } + +// 7. Poll. +{ "tool": "apps.deployments", "params": { "uuid": "" } } +// → [{ uuid, status: "finished" | "in_progress" | "failed" | "queued" }] +``` + +The agent hands the user back `https://my-site.mark.vibnai.com`. + +### 8.2 "Add an `api` subdomain to my app" + +```json +{ "tool": "apps.domains.set", + "params": { + "uuid": "", + "domains": ["my-site.mark.vibnai.com", "api.mark.vibnai.com"] + } } +``` + +Valid — both end with `.mark.vibnai.com`. `evil.com` or `my-site.justine.vibnai.com` +would return 403. + +### 8.3 "Delete the whole thing" + +Agent must learn the resource names first (or it'll hit the confirm gate): + +```json +// Learn the name. +{ "tool": "apps.get", "params": { "uuid": "" } } +// → { name: "my-site", ... } + +// Delete with matching confirm. +{ "tool": "apps.delete", + "params": { "uuid": "", "confirm": "my-site" } } +``` + +Wrong confirm returns `409 "Confirmation required"`. + +--- + +## 9. Error handling reference + +| Status | Meaning | What the agent should do | +|---|---|---| +| 400 | Bad request body (invalid JSON, missing required field, invalid type). | Fix the body, retry. | +| 401 | No / bad bearer token. | Ask the user to mint a fresh key. | +| 403 | **Tenant mismatch** — resource belongs to another workspace, domain outside workspace namespace, or repo not in workspace org. | **Stop.** Do not retry with guessed values. Ask the user. | +| 404 | Resource not found (app/db/service/repo uuid wrong). | Re-list to find the right uuid. | +| 409 | Delete confirmation missing or wrong. | Fetch the resource name first, then retry with `confirm=`. | +| 422 | Coolify validation failure (e.g. malformed domain). | Check the `details` field. | +| 502 | Upstream Coolify/Gitea error. | Retry with backoff. | +| 503 | Workspace not fully provisioned yet. | Call `POST /provision`, then retry. | + +--- + +## 10. Versioning + +The MCP descriptor at `GET /api/mcp` reports a semver `version`. Tool names +are append-only within a major version — agents can cache the tool list +safely for the duration of a conversation but should re-fetch on 404. + +Current version: **2.1.0**. + +- **1.x** — session-cookie-only MCP, no tenant keys. +- **2.0** — `vibn_sk_…` keys, workspace-scoped Gitea bot + Coolify project. +- **2.1** — create/update/delete for apps, 8 database flavors, auth + provider allowlist, domain policy enforcement, confirm-gated deletes. + +--- + +## 11. Where to look in the code + +- `lib/auth/workspace-auth.ts` — `requireWorkspacePrincipal`, the gate. +- `lib/auth/secret-box.ts` — AES-256-GCM encryption of Gitea PATs. +- `lib/workspaces.ts` — `ensureWorkspaceProvisioned` (the idempotent setup). +- `lib/gitea.ts` — Gitea client (orgs, users, PATs, SSH keys). +- `lib/coolify.ts` — Coolify client, tenant helpers, all resource CRUD. +- `lib/naming.ts` — domain policy, slugify, SSH URL templates. +- `lib/ssh-keys.ts` — ed25519 keypair generation + OpenSSH formatting. +- `app/api/workspaces/[slug]/…` — REST surface. +- `app/api/mcp/route.ts` — MCP dispatcher and tool implementations. +- `components/workspace/WorkspaceKeysPanel.tsx` — settings UI. diff --git a/AI_CAPABILITIES_ROADMAP.md b/AI_CAPABILITIES_ROADMAP.md new file mode 100644 index 0000000..36a0e3c --- /dev/null +++ b/AI_CAPABILITIES_ROADMAP.md @@ -0,0 +1,667 @@ +# Vibn AI Capability Roadmap + +> The ordered plan for closing the gap between what the Vibn agent can do +> today and what it needs to do for a real customer to ship, operate, and +> scale a SaaS through it. +> +> **Companion to:** [`AI_CAPABILITIES.md`](./AI_CAPABILITIES.md) (current state). +> +> **Prioritization framing:** +> 1. Does it unblock *shipping a real product* (not a demo)? +> 2. Does it unblock *surviving past the first paying customer*? +> 3. Does it only matter once usage scales? +> +> Tier 1 = (1). Tier 2 = (2). Tier 3 = (3). Tier 4 = revisit when demanded. +> +> **Sequencing rule:** complete Tier 1 before any Tier 2 item. The trap +> is polishing safety rails (audit, scopes, quotas) before the product is +> actually shippable. + +--- + +## 0. Substrate & constraints + +Vibn runs on a two-cloud substrate, constrained to Canadian data residency: + +| Layer | Provider | Region | Purpose | +|---|---|---|---| +| **App hosting** | Coolify (self-managed) | Montreal VPS | All app / database / auth containers. Current state. | +| **Managed services** | **Google Cloud** | `northamerica-northeast1` (Montreal) | Object storage, cron, queues, logs, backups, monitoring, secrets. | +| **Domain registration** | OpenSRS (Tucows) | Toronto | Wholesale domain API. Canadian company, pre-funded float account. | +| **Authoritative DNS** | Cloud DNS (default) / CIRA D-Zone (strict) | Global anycast / Canadian | Managed DNS for workspace-owned domains. | +| **Transactional email** | Amazon SES | `ca-central-1` (Montreal) | No GCP equivalent; AWS's Canadian region keeps data in-country. | + +**Absolute rule: no customer data leaves Canada.** Every workspace-owned +resource (storage bucket, database, log bucket, task queue, scheduler +job, email message body) must be pinned to a Canadian region. + +### Why mix clouds? +- **Coolify stays** because we already built the workspace-scoped + provisioning around it (Phase 4). Migrating apps to Cloud Run is a + rewrite we don't need. +- **GCP-CA** fills every managed-service gap Coolify has. Cheaper and + more reliable than self-hosting MinIO/Loki/scheduler. +- **AWS SES for email** because GCP has no first-party transactional + email service and SES `ca-central-1` is the only credible + Canadian-resident managed option. +- **OpenSRS for domains** because it's the wholesale API behind most + Canadian registrars, and we already have the deposit. + +### Compliance upgrade path (Tier 4 territory) +For regulated customers (healthcare, financial, public sector): +- **Assured Workloads for Canada** on GCP — enforces Canadian personnel + access + data residency contractually. +- **CIRA D-Zone** instead of Cloud DNS — first-party Canadian managed DNS. +- Keep the SES and OpenSRS pieces as-is (already Canadian-resident). + +Document the caveat on a public trust page. Build the Assured-Workloads +variant when a real customer asks. + +--- + +## Current state (Phase 4 + P5.1 verified, Apr 2026) + +- Workspace tenancy: Gitea org + Coolify project + SSH deploy key per + workspace. +- Agent can: create repos, create apps, provision 8 database flavors, + deploy 8 vetted auth providers, manage env vars, deploy + poll, + update, delete (with `?confirm=`), set domains under + `*.{slug}.vibnai.com`. +- Control-plane MCP: 24 tools + full REST surface at `/api/mcp`. + API-key scoped per workspace. +- **P5.1 custom apex domains** — OpenSRS + Cloud DNS + Coolify + lifecycle (search / register / attach / inspect) shipped and + verified end-to-end against PROD GCP + OpenSRS sandbox + PROD + Coolify on `v4.0.0-beta.473` (2026-04-22). All 5 sub-systems green + in `smoke-attach-e2e.ts`: register → zone → A records → registrar + NS update → Coolify `fqdn` patch → cleanup. Required a server-side + config fix on `coolify-server-mtl` (proxy.type=TRAEFIK, + is_build_server=false) so `Server::isProxyShouldRun()` returns + true and the controller maps `domains` → `fqdn` — see + [`AI_CAPABILITIES.md`](./AI_CAPABILITIES.md) § 3.6 for the gory details. +- **Agent-runner stdio MCP bridge** — `vibn-agent-runner` now exposes + its full in-house toolkit (28 tools) outward over 5 stdio MCP + servers so external clients (Cursor, Claude Desktop, Goose) can + drive the same Coolify / Gitea / workspace / memory / search / + sub-agent surface as the internal Coder/PM/Marketing agents, with + shared protected-repo + protected-app guardrails. Every tool now + has a pure `*-api.ts` module, a registry wrapper for the in-process + loop, and an MCP server wrapper — single source of truth, verified + by `scripts/smoke-mcp.js`. +- Enforced: tenant isolation, domain policy, delete confirms, + secrets-at-rest encryption, protected-repo / protected-app guards. + +See [`AI_CAPABILITIES.md`](./AI_CAPABILITIES.md) (§ 3.6 for P5.1, +§ 3.7 for the stdio MCP bridge) for the complete current surface. + +--- + +## Tier 1 — Blocks shipping a real product + +Without these, anything the agent builds is *demo-shaped*. Ship these +next, in the recommended sequence below. + +### P5.1 · Custom apex domains via OpenSRS + +**Goal:** agent buys `mysaas.com` on the user's behalf and attaches it +to a Coolify app with automatic TLS. + +**Why now:** you already opened an OpenSRS reseller account with a $100 +float. Unlocks real branding, DKIM for email (P5.2 depends on this), +and gives you a revenue line (markup on domains). + +**Surface:** + +| Tool / endpoint | Purpose | +|---|---| +| `domains.search` | Live availability + suggestions via OpenSRS `lookup`. | +| `domains.check_price` | Per-TLD price from OpenSRS + markup. | +| `domains.register` | Debits workspace float, registers via OpenSRS. | +| `domains.list` | Workspace's owned domains. | +| `domains.renew` / `domains.transfer` | Lifecycle. | +| `domains.{name}.attach` | Attach to a Coolify app: DNS records + Coolify `fqdn` + Let's Encrypt. | +| `domains.{name}.detach` | Free a domain from an app, keep registration. | +| `domains.{name}.attach_status` | Polls DNS propagation + cert issuance (async). | + +**Infra:** +- **OpenSRS client** (their XML/SOAP or REST API). +- **Cloud DNS** for zone management (default). CIRA D-Zone available as a + workspace-level preference for strict-residency customers. +- **Workspace float ledger** (`vibn_workspace_billing_float`) — a + prepaid balance in CAD, debited on register/renew. Reconciled nightly + against the OpenSRS master deposit. +- `VIBN_OPENSRS_DEPOSIT_ACCOUNT` as the master float handle. + +**New columns** on `vibn_workspaces`: +- `preferred_dns_provider TEXT DEFAULT 'cloud_dns'` +- `cloud_dns_zone_name TEXT` ← GCP managed zone for this workspace. + +**Risks:** +- DNS propagation is human-scale (minutes–hours). Agents need the + async `attach_status` polling loop, not a sync call. +- Cert issuance via Let's Encrypt is rate-limited (50/week per domain). + Abuse-prevent with per-workspace rate caps. + +**Estimate:** **2 weeks.** + +--- + +### P5.2 · Transactional email (AWS SES `ca-central-1`) + +**Goal:** auth providers can send password-reset emails; agents can +`email.send` from `noreply@mysaas.com`. + +**Why now:** every auth provider on the allowlist is broken without +SMTP. Also pairs with P5.1 — per-workspace sender domains need DKIM on +domains you own. + +**Why SES ca-central-1 specifically:** GCP has no first-party +transactional email service. All mainstream providers (Postmark, +Resend, Mailgun, SendGrid) are US-primary. SES's Montreal region is the +only credible managed option that keeps message bodies in Canada. + +**Two-phase rollout:** + +**Phase A — shared-sender MVP (1 week):** +- One SES-verified sender domain `mail.vibnai.com`. +- Every workspace can send from `noreply@mail.vibnai.com` out of the box. +- `email.send` tool + injected `SMTP_*` env vars. +- Bounce / complaint webhooks routed via SNS → a Cloud Run service + that writes per-workspace notifications. + +**Phase B — per-workspace sender domains (1 week, depends on P5.1):** +- `email.verify_sender_domain` creates the SPF/DKIM/DMARC records via + the Cloud DNS / CIRA D-Zone client on a workspace-owned domain. +- Polls SES verification; flips `verified=true` when done. +- Workspace can now `email.send from: founder@mysaas.com`. + +**Surface:** + +| Tool | Purpose | +|---|---| +| `email.send` | Single message; returns SES `message_id`. | +| `email.send_batch` | Up to 100 at a time. | +| `email.list_messages` | Recent sent mail + delivery state (from SES + our log). | +| `email.verify_sender_domain` | Kick off DKIM for a workspace-owned domain. | +| `email.sender_status` | Poll verification state. | +| `email.webhooks.list` | Recent bounces/complaints. | + +**Infra:** +- SES identity per workspace-owned sender domain. +- SNS topic → Cloud Run webhook receiver (in `northamerica-northeast1`) + for bounce/complaint ingestion. +- Rate limits: start in SES sandbox (200/day), request production limits + after first real customer. + +**Estimate:** **2 weeks total** (1 week Phase A + 1 week Phase B). + +--- + +### P5.3 · Object storage (Google Cloud Storage, `northamerica-northeast1`) + +**Goal:** any SaaS the agent builds can take user uploads — avatars, +attachments, exports, images — without the user pasting in third-party +credentials. + +**Why now:** "can users upload a file?" is the #1 post-demo question. +Blocks ~half of realistic SaaS ideas. + +**GCP collapses this item.** No MinIO container to babysit; GCS provides +managed bucket + signed URLs + lifecycle policies + encryption out of +the box. + +**Surface:** + +| Tool | Purpose | +|---|---| +| `storage.buckets.list` | Buckets in this workspace (filtered by `workspace={slug}` label). | +| `storage.buckets.create` | New bucket. Optional `public_read`. Enforced region: `northamerica-northeast1`. | +| `storage.buckets.delete` | Destroy bucket. `confirm` gate. | +| `storage.presign_upload` | PUT URL, TTL, content-type constraint. | +| `storage.presign_download` | GET URL, TTL. | +| `storage.list_objects` | Pagination + prefix filter. | +| `storage.delete_object` | Single object. | +| `storage.set_lifecycle` | TTL delete, multipart cleanup, archive tiering. | + +**Provisioning additions:** +- Default bucket `vibn-ws-{slug}` created on workspace provision. +- Uniform bucket-level access enabled by default. +- Per-workspace GCP service account `vibn-ws-{slug}@...`, scoped to its + own bucket via `roles/storage.objectAdmin`. +- Keyfile stored encrypted (AES-256-GCM, same `VIBN_SECRETS_KEY`) in + `vibn_workspaces.gcp_service_account_key_encrypted`. + +**New columns** on `vibn_workspaces`: +- `gcs_bucket_name TEXT` +- `gcp_service_account_email TEXT` +- `gcp_service_account_key_encrypted BYTEA` + +**Env injection:** +- `STORAGE_ENDPOINT=https://storage.googleapis.com` +- `STORAGE_BUCKET={workspace-bucket-name}` +- `STORAGE_ACCESS_KEY`, `STORAGE_SECRET_KEY` (S3-compatible via GCS HMAC keys) + — auto-injected on app creation so agent code uses standard S3 SDKs. + +**Estimate:** **3 days.** + +--- + +### P5.4 · Workers, cron, and queues (Cloud Tasks + Cloud Scheduler + Cloud Run Jobs) + +**Goal:** agents can declare async workers, scheduled jobs, and queued +tasks. Anything that isn't a single `ports: 3000` web container. + +**Why now:** webhooks, retries, nightly cleanup, image processing, +email sending — every real SaaS needs a non-web process. Current +workaround (second Coolify app) is brittle and manual. + +**Hybrid approach — Coolify for compute, GCP for orchestration:** + +Option evaluated and chosen: +- **Cloud Scheduler** (`northamerica-northeast1`) for cron: fires + HTTP webhooks into the app at the scheduled time. +- **Cloud Tasks** (`northamerica-northeast1`) for queue: agent code + calls `enqueue(task)`, Cloud Tasks dispatches to the app's worker + endpoint with retries, backoff, and at-least-once semantics. +- **Worker process** stays on Coolify as a second app-per-repo with a + different start command, exposed on an internal URL. + +Rejected alternative: migrate everything to Cloud Run Jobs. More managed +but splits the "Live" view across two deploy targets and changes the +agent's mental model. Not worth it for MVP. + +**Shape — extend `apps.create`:** + +```json +{ + "repo": "my-site", + "services": { + "web": { "command": "npm start", "ports": "3000" }, + "worker": { "command": "npm run worker", "replicas": 2 } + }, + "cron": [ + { "name": "nightly-backup", "schedule": "0 3 * * *", "path": "/tasks/backup" }, + { "name": "sync", "schedule": "*/10 * * * *", "path": "/tasks/sync" } + ], + "queues": [ + { "name": "emails" }, + { "name": "image-processing" } + ] +} +``` + +Internally creates: two Coolify apps (web + worker), N Cloud Scheduler +jobs labeled `workspace={slug}`, N Cloud Tasks queues. + +**Surface additions:** + +| Tool | Purpose | +|---|---| +| `apps.services.list` | All processes in an app. | +| `apps.services.update` | Scale replicas, change command. | +| `apps.services.logs` | Per-process logs. | +| `cron.list` | Scheduler jobs in this workspace. | +| `cron.create` / `cron.update` / `cron.delete` | Manage scheduled jobs. | +| `cron.run_now` | Fire a scheduled job immediately (useful for agent testing). | +| `queues.list` | Cloud Tasks queues in this workspace. | +| `queues.create` / `queues.delete` | Manage queues. | +| `queues.enqueue` | (Normally called from app code, but exposed for agent-driven testing.) | +| `queues.pause` / `queues.resume` | Emergency ops. | + +**New columns** on `vibn_workspaces`: +- `cloud_scheduler_location TEXT DEFAULT 'northamerica-northeast1'` +- `cloud_tasks_location TEXT DEFAULT 'northamerica-northeast1'` + +**Auth to GCP:** per-workspace service account (provisioned in P5.3) is +extended with `roles/cloudscheduler.admin` and `roles/cloudtasks.admin` +*scoped to resources labeled `workspace={slug}`* via IAM conditions. +Agents can only act on their own workspace's jobs/queues. + +**Estimate:** **1 week.** + +--- + +### Tier 1 total: ~5 weeks of focused work + +After Tier 1 lands, an agent can: +- Buy `mysaas.com`, point it at a Next.js app. +- Deploy Authentik with working password-reset emails from `noreply@mysaas.com`. +- Offer user uploads (avatars, attachments). +- Run `0 3 * * *` nightly cleanup cron. +- Process Stripe webhooks idempotently via a retry queue. + +That's a shippable SaaS. Everything after this is about *keeping* it +shipped. + +--- + +## Tier 2 — Blocks surviving past the first real customer + +Once users exist, these prevent silent failures. + +### P6.1 · Database backups + restore (GCS + wal-g) + +**Goal:** nightly backups, on-demand backups, one-call restore. No +"agent ran `DROP TABLE` in a migration" permanent data loss. + +**Why:** scariest item on this list. Failure mode is irrecoverable. + +**Shape:** +- `databases.{uuid}.backup` — on-demand `pg_dump` / `mongodump` to the + workspace's GCS bucket (depends on P5.3). +- `databases.{uuid}.backups.list` — lists backups with timestamp + size. +- `databases.{uuid}.backups.restore` — `confirm`-gated restore from a + specific backup uuid. +- Per-database backup policy: daily / hourly / off, retention days. +- Default: every AI-created database gets daily backups + 7-day + retention on. + +**Infra:** +- Cron jobs run via P5.4's Cloud Scheduler primitive. +- Stored at `gs://vibn-ws-{slug}/backups/{db-uuid}/{iso-timestamp}.sql.gz`. +- Lifecycle rules auto-delete backups older than retention. +- Object-level retention lock available for "immutable backups" on + request (Tier 3 feature). + +**Upgrade path:** +- **Postgres point-in-time recovery** via `wal-g` shipping WAL segments + to the same GCS bucket. Adds RPO < 5 min. +- **ClickHouse**: `clickhouse-backup` to GCS. +- **MongoDB**: `mongodump` incremental. + +**Estimate:** **3 days** for MVP (pg_dump + schedule + restore). +**+1 week** for wal-g PITR if/when a customer asks. + +--- + +### P6.2 · Runtime log streaming (Cloud Logging) + +**Goal:** agent can see "is the app erroring at 10 req/s right now?", +not just "did the build succeed." + +**Why:** today deploy logs are surfaced but container stdout/stderr is +not. An agent that "fixed a bug" can't verify the fix without a human +SSH-ing into Coolify. + +**GCP collapses this item** — ship container logs to Cloud Logging with +a workspace label, query via the logs API. + +**Shape:** +- Fluent-bit sidecar (or Coolify label) ships container stdout/stderr + to Cloud Logging in `northamerica-northeast1` with labels + `workspace={slug}`, `app={app-uuid}`, `service={web|worker|...}`. +- Per-workspace log bucket for retention isolation. + +**Surface:** + +| Tool | Purpose | +|---|---| +| `apps.logs` | Last N lines across replicas. Filter by timestamp, severity. | +| `apps.logs.tail` | SSE stream of new log lines. | +| `apps.logs.search` | Thin wrapper on Cloud Logging's query API — grep, severity filter, time window. | +| `apps.services.logs` | Same, scoped to a single service. | + +**Retention:** default 30 days in the workspace log bucket; exportable +to the workspace's GCS bucket on request for long-term storage. + +**Estimate:** **3 days** (fluent-bit config + thin API wrapper). + +--- + +### P6.3 · Scoped API keys + +**Goal:** invite a CI bot or teammate without giving root on the +workspace. + +**Why:** solo-builder flow survives without it. Breaks the moment a +second principal enters. + +**Shape:** +- Keys gain `scopes: string[]` and optional `expires_at`. +- Scope tokens: `apps:read`, `apps:write`, `apps:delete`, + `databases:*`, `auth:*`, `domains:read`, `domains:write`, + `storage:*`, `email:send`, `cron:*`, `queues:*`, `deploy:*`. +- Per-scope rate limits optional (Tier 3; API shape supports it from + day one). + +**Surface changes:** + +| Tool | Change | +|---|---| +| `keys.create` | Accepts `scopes`, `expires_at`. | +| `keys.list` | Returns scopes per key. | +| `keys.rotate` | Mints new token, preserves scope set. | + +Every MCP/REST handler gets a scope requirement checked in the +principal resolver. + +**Estimate:** **1 week.** + +--- + +### Tier 2 total: ~2 weeks + +After Tier 2 lands, a SaaS shipped on Vibn can survive without you +dropping into a psql REPL at 3am. + +--- + +## Tier 3 — Matters once usage scales + +Don't build these until at least one real customer is hitting them. +Building them pre-market is the classic infra-overinvestment trap. + +### P7.1 · Per-workspace quotas + cost caps +Max apps, max dbs, max GCS GB, max egress, max SES messages/month, max +OpenSRS spend/month. Per-plan configurable. Hallucinating agents can't +OOM the cluster or burn your SES reputation. + +### P7.2 · Audit log +Append-only per-workspace log of (principal, action, params, timestamp, +result). Cloud Logging with a dedicated `audit-logs` log-bucket, 400-day +retention. Read API for the settings panel. Needed for any +SOC-2-adjacent buyer. + +### P7.3 · Preview-per-PR environments +Open a PR → `pr-42.mark.vibnai.com` deploys automatically with a +throw-away database. Teardown on PR close/merge. Unblocks multi-agent +flows. + +### P7.4 · Atomic multi-resource operations (`stacks`) +`POST /stacks` takes a full app + db + auth + domain + cron spec; +creates atomically, rolls back on failure. Agent ergonomics win once +demo flow is routine. + +### P7.5 · Billing integration +Stripe subscriptions for Vibn itself (workspace billing), plus +per-workspace float top-ups, plus reconciliation to the OpenSRS master +deposit and GCP / SES cost allocation. Only needed when you charge +real dollars. + +### P7.6 · Assured Workloads for Canada +GCP policy-enforced Canadian residency + Canadian personnel access. +For regulated customers (healthcare, financial, public sector). Priced +accordingly; ship only when a real customer needs it. + +### P7.7 · CIRA D-Zone as a workspace DNS option +Swap Cloud DNS → CIRA D-Zone for a workspace with strict residency +requirements. API-compatible wrapper so nothing agent-facing changes. + +--- + +## Tier 4 — Revisit when demanded + +Items to explicitly *not* build until a concrete customer asks. + +- **Multi-region** — single-region Canada is fine for B2B SaaS makers + (our early market). +- **Cloud Run migration** — would rewrite most of Coolify-based + capabilities. Revisit if/when Coolify becomes a bottleneck. +- **Managed search / vector DB as first-class types** — agents can + deploy Meilisearch / Typesense / pgvector-Postgres as regular services. +- **mTLS / custom CAs / BYO-cert upload** — enterprise creep. +- **MCP protocol polish** (streaming, resources, prompts, per-tool + schemas) — current JSON-over-HTTP works. Revisit on real friction. +- **Per-app basic auth, IP allowlists, WAF** — Traefik middleware + manually until someone asks. + +--- + +## Roadmap at a glance + +| Phase | Items | Est. | Unblocks | +|---|---|---|---| +| **P5 — Real SaaS primitives** | Domains, email, storage, workers/cron/queues | ~5 wk | Shipping a real product | +| **P6 — Keep-it-running** | Backups, runtime logs, scoped keys | ~2 wk | First real customer survives | +| **P7 — Scale** | Quotas, audit, previews, stacks, billing, Assured Workloads, D-Zone | demand-driven | Platform grows past 1st cohort | +| **P8+** | Tier 4 items | never, unless pulled by customer | — | + +**Total to "agent ships a SaaS a founder would pay $29/mo for":** +P5 + P6 = **~7 weeks** (was ~11 before GCP-CA; ~40% compression from +managed-service leverage). + +--- + +## Dependency graph + +``` +P5.1 Domains ──┬──→ P5.2 Email Phase B (per-domain DKIM) + ├──→ P7.7 CIRA D-Zone swap + └──→ (future: customer-owned sub-domain routing) + +P5.3 Storage ──┬──→ P6.1 Database backups (backups need a bucket) + └──→ P7.2 Audit log export + +P5.4 Workers/cron/queues ──┬──→ P6.1 Database backups (run via scheduler) + └──→ most real SaaS patterns + +P6.2 Runtime logs — independent, can land anytime +P6.3 Scoped keys — independent, can land anytime +P7.6 Assured Workloads — wraps everything; build once demanded +``` + +**Parallelizable (three people):** +- Track A: P5.1 → P5.2 +- Track B: P5.3 → P6.1 +- Track C: P5.4 → P6.2 + +Track C finishes earliest; use that slack to land P6.3. + +--- + +## Per-workspace GCP provisioning (shared across P5.3, P5.4, P6.1, P6.2) + +`ensureWorkspaceProvisioned()` gains a GCP-CA block that runs once per +workspace, idempotently. All resources are created in +`northamerica-northeast1`. + +| Resource | Name pattern | Notes | +|---|---|---| +| GCS bucket | `vibn-ws-{slug}` | Uniform bucket-level access. Lifecycle policies off by default. | +| Cloud DNS managed zone | `vibn-ws-{slug}-zone` | Created per workspace-owned domain in P5.1, not on workspace provision. | +| Cloud Logging log bucket | `vibn-ws-{slug}-logs` | 30-day retention default. | +| Cloud Tasks location | `northamerica-northeast1` | Queues created per-app in P5.4, not here. | +| GCP service account | `vibn-ws-{slug}@{project}.iam` | Single SA per workspace, narrow roles. | +| Service account key | stored encrypted in `vibn_workspaces` | AES-256-GCM, same `VIBN_SECRETS_KEY`. | + +**New columns** on `vibn_workspaces` (cumulative across P5.1-P6.2): + +```sql +-- P5.1 +preferred_dns_provider TEXT DEFAULT 'cloud_dns', +cloud_dns_zone_name TEXT, + +-- P5.3 +gcs_bucket_name TEXT, +gcp_service_account_email TEXT, +gcp_service_account_key_encrypted BYTEA, + +-- P5.4 +cloud_scheduler_location TEXT DEFAULT 'northamerica-northeast1', +cloud_tasks_location TEXT DEFAULT 'northamerica-northeast1', + +-- P6.2 +cloud_logging_bucket_name TEXT +``` + +Three migration steps, one per phase. All guarded by the existing +admin-gated `POST /api/admin/migrate` endpoint. + +--- + +## Non-goals (stated explicitly so they don't creep in) + +- **A general-purpose PaaS.** Vibn is an agent-driven SaaS builder, not + a Heroku / Fly clone. Every capability must answer "what does an agent + need to build a SaaS?" — not "what does a dev need to deploy a + container?" +- **Support for non-allowlisted auth providers, databases, services.** + The curated surface is the feature. "Any Coolify service" would blow + up the tenant-safety model and dilute agent decision-making. +- **A consumer-facing OpenSRS UI.** OpenSRS is plumbing for the agent. + Humans should never see an OpenSRS checkout screen — only + `domains.register { name: "mysaas.com" }` from the agent. +- **Multi-cloud abstraction layer.** One Coolify cluster + GCP-CA + + SES-CA + OpenSRS is the contract. If customers want to bring their + own, that's Tier 4. +- **Anything that moves customer data out of Canada.** Even for + performance. If a managed service only has US regions, we self-host + in Canada or we don't offer it. + +--- + +## Recommended execution order (opinionated) + +Given dependencies and quick-wins-first philosophy: + +**Week 1:** +- P5.3 Storage (GCS wrap, 3 days) → proves the GCP-CA provisioning pattern. +- P5.4 Workers/cron/queues (starts in parallel; depends on P5.3 only for + the service account). + +**Week 2:** +- P5.4 completes. +- P5.1 Domains starts (OpenSRS client + Cloud DNS wrapper). + +**Week 3:** +- P5.1 completes. +- P5.2 Email Phase A (shared-sender MVP) starts. + +**Week 4:** +- P5.2 Phase A completes. +- P5.2 Phase B (per-domain DKIM) starts, now that P5.1 is available. + +**Week 5:** +- P5.2 Phase B completes. **P5 / Tier 1 done.** +- P6.1 Database backups starts (3 days). +- P6.2 Runtime logs starts in parallel (3 days). + +**Week 6:** +- P6.3 Scoped keys (1 week). + +**Week 7:** +- Slack week — hardening, docs (`AI_CAPABILITIES.md` refresh), first + real customer onboarding. + +**End state at week 7:** agent can take a founder from "I have an idea" +to "I have `mysaas.com` live, with auth, with user uploads, with email, +with backups, with visible error logs, and a CI bot can deploy it +without root access." + +That's the Vibn product. + +--- + +## How to use this doc + +- When someone proposes a feature, find its tier. If it's Tier 3 or 4 + and we're still shipping Tier 1, say no. +- Before starting a Tier 1 item, re-read its section and make sure + prerequisites shipped. Email-per-domain before domains is wasted code. +- [`AI_CAPABILITIES.md`](./AI_CAPABILITIES.md) is the canonical + reference of *what exists today*. This doc is the canonical reference + of *what comes next*. When an item ships, move it from here to that + doc and delete its section here. +- When a user request implies Canadian residency (they say "PIPEDA", + "healthcare", "public sector", or "our data can't leave Canada"), pin + the answer to this doc's §0 Substrate & constraints. Don't improvise. diff --git a/README.md b/README.md deleted file mode 100644 index b75416d..0000000 --- a/README.md +++ /dev/null @@ -1,138 +0,0 @@ -# Product OS - Master AI - -> A Product-Centric IDE built on Eclipse Theia, optimized for Google Cloud and powered by Gemini AI. - -## Overview - -Product OS is NOT a general-purpose IDE. It's a **Product Operating System** designed to unify: - -- 💻 **Code** - Build and deploy Cloud Run services -- 📢 **Marketing** - Automate campaigns and content -- 📊 **Analytics** - Product intelligence and insights -- 🚀 **Growth** - Optimize onboarding and conversion -- 💬 **Support** - Customer feedback integration -- 🧪 **Experiments** - A/B testing and rollouts -- 🏗️ **Infrastructure** - Production monitoring - -All powered by AI and optimized for Google Cloud. - -## Vision - -See [Google_Cloud_Product_OS.md](./Google_Cloud_Product_OS.md) for complete vision and requirements. - -## Project Structure - -``` -master-ai/ -├── theia/ # Customized Eclipse Theia IDE -│ ├── packages/core/ # UI shell customization -│ ├── packages/terminal/ # Terminal customization -│ ├── packages/monaco/ # Editor themes -│ └── examples/electron/ # Mac app build -├── Google_Cloud_Product_OS.md # Product vision & requirements -├── UI-DESIGN-GUIDE.md # Design customization guide -└── README.md # This file -``` - -## Getting Started - -### Prerequisites - -- Node.js 20.x (use nvm) -- Python 3 -- Git - -### Installation - -```bash -# Clone this repo -git clone https://github.com/MawkOne/master-ai.git -cd master-ai/theia - -# Use Node 20 -nvm use 20 - -# Install dependencies -npm install - -# Build Electron app -npm run build:electron - -# Launch Product OS -npm run start:electron -``` - -## Customization - -See [UI-DESIGN-GUIDE.md](./UI-DESIGN-GUIDE.md) for complete customization guide. - -### Quick Design Changes - -**Change colors:** Edit `theia/packages/monaco/data/monaco-themes/vscode/dark_vs.json` - -**Change fonts:** Edit `theia/packages/core/src/browser/style/index.css` - -**Change layout:** Edit `theia/packages/core/src/browser/shell/application-shell.ts` - -## Development Workflow - -```bash -cd theia - -# Watch mode (auto-rebuild on changes) -npm run watch:electron - -# In another terminal, run the app -npm run start:electron -``` - -## Building Distribution - -```bash -cd theia/examples/electron - -# Build Mac app (.app, .dmg) -npm run package:mac - -# Output: dist/Product-OS-*.dmg -``` - -## Roadmap - -### Phase 1: Core Simplification ✅ -- [x] Clone Theia -- [x] Set up repository -- [ ] Lock dark theme -- [ ] Remove preference menus -- [ ] Simplify UI - -### Phase 2: Custom Layout -- [ ] Replace Application Shell structure -- [ ] Create 7 Product OS sections -- [ ] Custom navigation/activity bar -- [ ] Section-specific views - -### Phase 3: Google Cloud Integration -- [ ] Pre-configure GCP tools -- [ ] Add Gemini AI integration -- [ ] Remove non-GCP options - -### Phase 4: Product OS Features -- [ ] Marketing automation UI -- [ ] Analytics dashboards -- [ ] Growth experiment tools -- [ ] Support integration - -## Architecture - -Built on [Eclipse Theia](https://github.com/eclipse-theia/theia) - an extensible IDE framework using TypeScript and Electron. - -## License - -Based on Eclipse Theia: -- Eclipse Public License 2.0 -- GNU General Public License, version 2 with the GNU Classpath Exception - -## Author - -Built by [@MawkOne](https://github.com/MawkOne) diff --git a/TURBOREPO_MIGRATION_PLAN.md b/TURBOREPO_MIGRATION_PLAN.md index 92bc80d..e3bb003 100644 --- a/TURBOREPO_MIGRATION_PLAN.md +++ b/TURBOREPO_MIGRATION_PLAN.md @@ -4,226 +4,206 @@ The core thesis of this platform is that **one AI controls everything in one project**. For that to work, the AI needs a complete mental model of the project — all apps, all shared code, all dependencies — in a single coherent context. -The current architecture creates separate Gitea repos per app (frontend repo, API repo, etc.), which fragments that context. The AI has to context-switch across repos, cross-repo dependencies are manual and brittle, and shared code has no clean home. +The current architecture creates a single Gitea repo per project with no enforced internal structure. The AI has no reliable way to know where apps live, what shares code with what, or how to trigger a targeted build for one part of the project. -By adopting **Turborepo monorepo per project**, every project becomes a single repo containing all of its apps (`product`, `website`, `admin`) and shared packages (`ui`, `types`, `config`). The AI operates across the entire project simultaneously. Build orchestration, deployment, and shared code all become coherent automatically. +By adopting **Turborepo monorepo per project**, every project repo gets a standardised structure containing all of its apps (`product`, `website`, `admin`, `storybook`) and shared packages (`ui`, `tokens`, `types`, `config`). The AI operates across the entire project simultaneously. Build orchestration, deployment, and shared code all become coherent automatically. -**The structure every project will have:** +**The structure every user project repo will have:** ``` -{project-slug}/ +{project-slug}/ ← one Gitea repo per project apps/ - product/ ← the core user-facing app - website/ ← marketing / landing site - admin/ ← internal admin tool + product/ ← core user-facing app (Next.js) + website/ ← marketing / landing site (Next.js) + admin/ ← internal admin tool (Next.js) + storybook/ ← component browser and design system packages/ - ui/ ← shared component library - types/ ← shared TypeScript types - config/ ← shared eslint, tsconfig + ui/ ← shared React component library + tokens/ ← design tokens (colors, spacing, typography) + types/ ← shared TypeScript types + config/ ← shared eslint, tsconfig turbo.json - package.json ← workspace root (pnpm workspaces) + package.json ← pnpm workspace root .gitignore README.md ``` -This is not a Vercel dependency. Turborepo is MIT-licensed, runs anywhere, and costs nothing. Remote caching is optional and can be self-hosted on Coolify. +Turborepo is MIT-licensed, runs anywhere, and costs nothing. No Vercel dependency. + +--- + +## Infrastructure Context + +Everything runs on a single GCP VM (`34.19.250.135`, Montreal) via Docker + Traefik: + +| Service | URL | Repo | +|---|---|---| +| Platform frontend | `vibnai.com` | `git.vibnai.com/mark/vibn-frontend` | +| Gitea | `git.vibnai.com` | — | +| Coolify | `coolify.vibnai.com` | — | +| PostgreSQL | internal | — | + +**All platform logic lives in `vibn-frontend`** (Next.js). There is no separate control plane service. The backend is Next.js API routes in `app/api/`. Storage is PostgreSQL via raw SQL queries (no ORM layer in use for project data). + +**Integrations that already exist and should not be replaced:** +- `lib/gitea.ts` — full Gitea API client (create repo, webhooks, signature verification) +- `lib/coolify.ts` — full Coolify API client (projects, databases, applications, deployments) +- `app/api/projects/create/route.ts` — project creation flow (creates Gitea repo) +- `app/api/webhooks/gitea/route.ts` — receives Gitea push/PR events +- `app/api/webhooks/coolify/route.ts` — receives Coolify deployment events +- `app/api/ai/chat/route.ts` — AI chat with Gemini +- `lib/auth/authOptions.ts` — NextAuth v4 with Prisma adapter --- ## Scope of Changes -### 1. Project Scaffold Templates +### 1. Scaffold Templates -**What:** Create a set of template files that get written into a new Gitea repo when a project is created. +**What:** A set of template files written into the user's Gitea repo when a project is created, giving every project the standard Turborepo monorepo structure. -**Files to create:** `platform/scripts/templates/turborepo/` +**Where:** `vibn-frontend/lib/scaffold/turborepo/` -- `turbo.json` — pipeline config defining `build`, `dev`, `lint`, `test` tasks and their dependencies -- `package.json` — workspace root with pnpm workspaces pointing to `apps/*` and `packages/*` -- `.gitignore` — covering node_modules, dist, .turbo cache -- `apps/product/package.json` — Next.js app skeleton -- `apps/website/package.json` — Astro or Next.js skeleton -- `apps/admin/package.json` — Next.js app skeleton -- `packages/ui/package.json` — shared component library stub -- `packages/types/package.json` — shared types stub -- `packages/config/` — shared `tsconfig.json` and `eslint` base configs +**Files to create:** +- `turbo.json` — pipeline: `build`, `dev`, `lint`, `type-check`, `test` +- `package.json` — pnpm workspace root pointing to `apps/*` and `packages/*` +- `.gitignore` +- `README.md` — project-specific (name injected at scaffold time) +- `apps/product/` — Next.js 15, references shared `ui`, `tokens`, `types` +- `apps/website/` — Next.js 15 +- `apps/admin/` — Next.js 15 +- `apps/storybook/` — Storybook 8 +- `packages/ui/` — Button, Card, Input, Badge components using CSS token vars +- `packages/tokens/` — design tokens as TS + CSS custom properties +- `packages/types/` — shared `User`, `ApiResponse`, `PaginatedResponse` types +- `packages/config/` — `tsconfig.base.json` and `eslint.config.js` -**Notes:** -- Templates should be stack-agnostic at the shell level — the `turbo.json` pipeline is what matters, inner frameworks can vary -- Stack choices (Next.js vs Astro, etc.) can be parameterised later when we add a project creation wizard +**Status:** Templates were written and are ready. Need to be moved to `vibn-frontend/lib/scaffold/turborepo/`. --- -### 2. Control Plane — Project Data Model Update +### 2. Project Creation Route — Add Scaffold Push -**What:** The current data model stores multiple Gitea repos per project. This changes to one repo per project. +**What:** The existing `app/api/projects/create/route.ts` already creates a Gitea repo. It needs one additional step: push the Turborepo scaffold as the initial commit. -**File:** `platform/backend/control-plane/src/types.ts` +**File to update:** `vibn-frontend/app/api/projects/create/route.ts` -**Changes:** -- Remove `repos: Array<{ gitea_repo, path }>` from `ProjectRecord` (or update it) -- Add `repo: string` — single Gitea repo URL for the project -- Add `apps: Array<{ name: string; path: string; coolify_service_uuid?: string }>` — tracks each app inside the monorepo and its Coolify service -- Add `turbo: { version: string }` — tracks which Turborepo version the project was scaffolded with +**Current flow:** +1. Create Gitea repo (`auto_init: true` — creates empty repo with README) +2. Register webhook +3. Save project record to PostgreSQL + +**New step to add after repo creation:** +- Read scaffold template files from `lib/scaffold/turborepo/` +- Replace `{{project-slug}}` and `{{project-name}}` placeholders +- Push each file to the Gitea repo via the contents API +- This replaces the default empty `auto_init` commit + +**Note:** Change `auto_init: true` to `auto_init: false` since we are pushing the scaffold ourselves. --- -### 3. Control Plane — New Project Routes +### 3. Project Data Model — Add App Tracking -**What:** Add project management endpoints to the control plane API. +**What:** The `fs_projects` table stores project data as a JSONB `data` column. The `data` object needs two new fields to track the monorepo apps and their Coolify services. -**File to create:** `platform/backend/control-plane/src/routes/projects.ts` +**Fields to add to the project `data` JSONB:** -**Endpoints:** +```typescript +apps: Array<{ + name: string; // "product" | "website" | "admin" | "storybook" + path: string; // "apps/product" + coolifyServiceUuid?: string; + domain?: string; +}> +turboVersion: string; // e.g. "2.3.3" +``` -| Method | Path | Purpose | -|--------|------|---------| -| `POST` | `/projects` | Create project — scaffold Turborepo repo in Gitea, register in DB | -| `GET` | `/projects/:project_id` | Get project record | -| `GET` | `/projects/:project_id/apps` | List apps within the monorepo | -| `POST` | `/projects/:project_id/apps` | Add a new app to the monorepo | -| `POST` | `/projects/:project_id/deploy` | Trigger Turbo build + Coolify deploy for one or all apps | - -**Project creation flow (`POST /projects`):** -1. Validate request (name, tenant_id, optional app selections) -2. Create Gitea repo via Gitea API -3. Scaffold Turborepo structure from templates, push initial commit -4. Register webhook: Gitea repo → control plane `/webhooks/gitea` -5. Create Coolify project -6. Create one Coolify service per app (with correct build filter) -7. Save project record to storage -8. Return project record with repo URL and app list +No schema migration needed — it's JSONB, just include these fields when inserting/updating. --- -### 4. Control Plane — Storage Layer Updates +### 4. Coolify — Per-App Service Provisioning -**What:** Add project storage operations alongside existing runs/tools storage. +**What:** When a project is created, each app in the monorepo gets its own Coolify service with the correct Turbo build filter. This extends the existing `lib/coolify.ts`. -**File to update:** `platform/backend/control-plane/src/storage/memory.ts` -**File to update:** `platform/backend/control-plane/src/storage/firestore.ts` -**File to update:** `platform/backend/control-plane/src/storage/index.ts` +**File to update:** `vibn-frontend/lib/coolify.ts` -**New operations to add:** -- `saveProject(project: ProjectRecord): Promise` -- `getProject(projectId: string): Promise` -- `listProjects(tenantId: string): Promise` -- `updateProjectApp(projectId: string, app: AppRecord): Promise` +**Add function:** + +```typescript +createMonorepoAppService(opts: { + projectUuid: string; + appName: string; // e.g. "product" + gitRepo: string; // the project's Gitea clone URL + domain: string; // e.g. "product-taskmaster.vibnai.com" +}): Promise +``` + +Build command: `pnpm install && turbo run build --filter={appName}` + +**Wire into project creation:** After Gitea repo is created and scaffold is pushed, create one Coolify service per app and store the `coolifyServiceUuid` in the project's `apps` array. --- -### 5. Gitea Integration Service +### 5. Deploy API Route -**What:** New service to abstract all Gitea API calls. Currently there is no Gitea integration in the control plane. +**What:** A new API route that triggers a Coolify deployment for a specific app within a project. -**File to create:** `platform/backend/control-plane/src/gitea.ts` +**File to create:** `vibn-frontend/app/api/projects/[projectId]/deploy/route.ts` -**Responsibilities:** -- Create repo for a project -- Push initial scaffolded files (initial commit) -- Register webhooks -- Read file tree (so AI can understand the project structure) -- Read/write individual files (so AI can make edits) +``` +POST /api/projects/{projectId}/deploy +Body: { app_name: "product" | "website" | "admin" | "storybook" } +``` -**Config needed in `config.ts`:** -- `giteaUrl` — from `GITEA_URL` env var (e.g. `https://git.vibnai.com`) -- `giteaToken` — from `GITEA_TOKEN` env var (admin token for repo creation) +Flow: +1. Load project from PostgreSQL +2. Find the app's `coolifyServiceUuid` +3. Call `deployApplication(uuid)` from `lib/coolify.ts` +4. Return deployment UUID --- -### 6. Coolify Integration Service +### 6. AI Chat — Project Context Injection -**What:** New service to abstract all Coolify API calls. Currently the deploy executor calls Coolify but there is no central integration. +**What:** The existing `app/api/ai/chat/route.ts` handles Gemini chat. It needs to inject monorepo structure context when a `projectId` is present in the request. -**File to create:** `platform/backend/control-plane/src/coolify.ts` +**File to update:** `vibn-frontend/app/api/ai/chat/route.ts` -**Responsibilities:** -- Create a Coolify project -- Create a Coolify application service linked to a Gitea repo -- Set the build command to `turbo run build --filter={app-name}` -- Set the publish directory per app -- Trigger a deployment -- Get deployment status - -**Config needed in `config.ts`:** -- `coolifyUrl` — from `COOLIFY_URL` env var -- `coolifyToken` — from `COOLIFY_TOKEN` env var - ---- - -### 7. Deploy Executor — Monorepo Awareness - -**What:** The existing deploy executor (`platform/backend/executors/deploy`) currently deploys a single service. It needs to understand the monorepo structure and use `turbo run build --filter` to target the right app. - -**File to update:** `platform/backend/executors/deploy/src/index.ts` - -**Changes:** -- Accept `app_name` in the input payload (e.g. `"product"`, `"website"`, `"admin"`) -- Build command becomes `turbo run build --filter={app_name}` instead of `npm run build` -- Pass the root of the monorepo as the build context, not an app subdirectory - ---- - -### 8. AI Context — Project-Aware Prompting - -**What:** The Gemini chat integration currently has no awareness of which project the user is in. It needs project context so the AI can reason across the whole monorepo. - -**File to update:** `platform/backend/control-plane/src/gemini.ts` -**File to update:** `platform/backend/control-plane/src/routes/chat.ts` - -**Changes:** -- Add `project_id` to `ChatRequest` -- On chat requests with a `project_id`, fetch the project record and inject: - - Repo structure (app names, package names) - - Recent deployment status per app - - `turbo.json` pipeline config -- Add a new Gemini tool: `scaffold_app` — lets the AI add a new app to the user's monorepo -- Add a new Gemini tool: `deploy_app` — lets the AI trigger a Coolify deploy for a specific app by name - ---- - -### 9. Theia Workspace — Single Repo Mode - -**What:** The current Theia docker-compose opens a multi-root workspace across multiple repos. With one repo per project, this simplifies to a single workspace root. - -**File to update:** `theia-docker-compose.yml` (and the Coolify service config for Theia) - -**Changes:** -- Workspace path points to the cloned monorepo root -- Git remote is the project's single Gitea repo -- Theia extensions should be aware of the `turbo.json` to surface run targets in the UI (future) - ---- - -### 10. Local Dev — Replace start-all.sh with Turbo - -**What:** The current `platform/scripts/start-all.sh` manually starts each service with `&`. Once the platform itself is in a Turborepo, this can be replaced with `turbo run dev`. - -**Note:** This is a nice-to-have follow-on. The priority is getting user project scaffolding right first. The platform's own internal structure can be migrated to Turborepo in a separate pass. +**Add to chat request handling:** +- Accept optional `projectId` +- When present, load the project from PostgreSQL +- Inject into the system prompt: + - Project name, slug, repo URL + - List of apps and their domains + - Shared packages available + - Turbo version and build command pattern +- Add two new Gemini tools: + - `deploy_app` — triggers `POST /api/projects/{projectId}/deploy` + - `scaffold_app` — adds a new app folder to the monorepo via Gitea contents API --- ## Implementation Order -| Step | Task | Depends On | -|------|------|-----------| -| 1 | Create scaffold templates | Nothing | -| 2 | Add `ProjectRecord` type + storage ops | Step 1 | -| 3 | Build Gitea integration service | Step 2 | -| 4 | Build Coolify integration service | Step 2 | -| 5 | Add project routes to control plane | Steps 2, 3, 4 | -| 6 | Update deploy executor for monorepo | Step 5 | -| 7 | Update AI chat with project context | Step 5 | -| 8 | Update Theia workspace config | Step 5 | -| 9 | Migrate platform itself to Turborepo | All of the above | +| Step | Task | File | Depends On | +|------|------|------|-----------| +| 1 | Move scaffold templates into `vibn-frontend/lib/scaffold/` | `lib/scaffold/turborepo/**` | — | +| 2 | Update project creation to push scaffold | `app/api/projects/create/route.ts` | Step 1 | +| 3 | Add app tracking fields to project data | `app/api/projects/create/route.ts` | Step 2 | +| 4 | Add `createMonorepoAppService` to Coolify lib | `lib/coolify.ts` | — | +| 5 | Wire Coolify per-app provisioning into project creation | `app/api/projects/create/route.ts` | Steps 3, 4 | +| 6 | Add deploy route | `app/api/projects/[projectId]/deploy/route.ts` | Step 4 | +| 7 | Inject monorepo context into AI chat | `app/api/ai/chat/route.ts` | Step 3 | --- ## What Does Not Change -- Gitea as the source control host — same, just one repo per project instead of many -- Coolify as the deployment host — same, just configured with Turbo build filters -- Theia as the IDE — same, just opens one repo instead of multi-root -- The control plane API architecture (Fastify, in-memory/Firestore storage) — same, just extended -- Auth model — unchanged -- No Vercel dependency anywhere in this plan +- Gitea as source control — same, one repo per project (already the case) +- Coolify as deployment host — same, extended with per-app services +- NextAuth for auth — unchanged +- PostgreSQL + JSONB for project storage — unchanged +- `lib/gitea.ts` and `lib/coolify.ts` — extended, not replaced +- No Vercel dependency anywhere diff --git a/UI-DESIGN-GUIDE.md b/UI-DESIGN-GUIDE.md deleted file mode 100644 index 6402f85..0000000 --- a/UI-DESIGN-GUIDE.md +++ /dev/null @@ -1,254 +0,0 @@ -# Product OS - UI Shell Design Guide - -## 🎨 Design Customization Map - -### **Quick Reference: Where to Change What** - -| What You Want to Change | File to Edit | Line/Section | -|------------------------|--------------|--------------| -| **Colors** | `packages/monaco/data/monaco-themes/vscode/dark_vs.json` | All colors | -| **Fonts** | `packages/core/src/browser/style/index.css` | Lines 40-62 | -| **Panel Sizes** | `packages/core/src/browser/shell/application-shell.ts` | Lines 2269-2288 | -| **Layout Structure** | `packages/core/src/browser/shell/application-shell.ts` | Lines 188-220 | -| **Borders & Spacing** | `packages/core/src/browser/style/index.css` | Lines 24-76 | - ---- - -## 1. COLOR CUSTOMIZATION - -### File: `packages/monaco/data/monaco-themes/vscode/dark_vs.json` - -```json -{ - "colors": { - "editor.background": "#1E1E1E", // Main editor area - "editor.foreground": "#D4D4D4", // Text color - - // SIDEBAR - "sideBar.background": "#252526", // Left/right sidebar background - "sideBarTitle.foreground": "#BBBBBB", // Sidebar titles - - // ACTIVITY BAR (left icon strip) - "activityBar.background": "#333333", // Activity bar background - "activityBarBadge.background": "#007ACC", // Badge colors (notifications) - - // PANELS (bottom area) - "panel.background": "#1E1E1E", // Bottom panel (terminal, etc) - "panel.border": "#454545", // Panel borders - - // TABS - "tab.activeBackground": "#1E1E1E", // Active tab - "tab.inactiveBackground": "#2D2D2D", // Inactive tabs - "tab.activeForeground": "#FFFFFF", // Active tab text - - // MENU - "menu.background": "#252526", // Menu background - "menu.foreground": "#CCCCCC", // Menu text - - // STATUS BAR (bottom) - "statusBar.background": "#007ACC", // Status bar background - "statusBar.foreground": "#FFFFFF", // Status bar text - - // BUTTONS - "button.background": "#0E639C", // Button background - "button.foreground": "#FFFFFF", // Button text - - // INPUTS - "input.background": "#3C3C3C", // Input fields - "input.foreground": "#CCCCCC", // Input text - "input.border": "#454545", // Input borders - } -} -``` - ---- - -## 2. TYPOGRAPHY - -### File: `packages/core/src/browser/style/index.css` - -```css -:root { - /* UI Font (menus, buttons, labels) */ - --theia-ui-font-size1: 13px; /* Base size */ - --theia-ui-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - - /* Code Font (editor, terminal) */ - --theia-code-font-size: 13px; - --theia-code-font-family: Menlo, Monaco, Consolas, "Droid Sans Mono"; - - /* Change to your preferred fonts: */ - /* --theia-ui-font-family: "Inter", "SF Pro", sans-serif; */ - /* --theia-code-font-family: "JetBrains Mono", "Fira Code", monospace; */ -} -``` - ---- - -## 3. SPACING & LAYOUT - -### File: `packages/core/src/browser/style/index.css` - -```css -:root { - --theia-ui-padding: 6px; /* General padding */ - --theia-border-width: 1px; /* Border thickness */ - --theia-panel-border-width: 1px; /* Panel borders */ - --theia-icon-size: 16px; /* Icon size */ -} -``` - -### File: `packages/core/src/browser/shell/application-shell.ts` - -```typescript -// Panel size ratios (line ~2269) -export const DEFAULT_OPTIONS = { - bottomPanel: { - initialSizeRatio: 0.382 // 38.2% of window height - }, - leftPanel: { - initialSizeRatio: 0.191 // 19.1% of window width - }, - rightPanel: { - initialSizeRatio: 0.191 // 19.1% of window width - } -} -``` - ---- - -## 4. QUICK DESIGN CHANGES - Copy & Paste - -### A. Modern Dark Blue Theme - -**File:** `packages/monaco/data/monaco-themes/vscode/dark_vs.json` - -```json -{ - "colors": { - "editor.background": "#0D1117", - "sideBar.background": "#161B22", - "activityBar.background": "#010409", - "activityBarBadge.background": "#1F6FEB", - "statusBar.background": "#1F6FEB", - "panel.background": "#0D1117", - "tab.activeBackground": "#0D1117", - "tab.inactiveBackground": "#161B22" - } -} -``` - -### B. Increase Font Sizes - -**File:** `packages/core/src/browser/style/index.css` - -```css -:root { - --theia-ui-font-size1: 15px; /* Was 13px */ - --theia-code-font-size: 15px; /* Was 13px */ -} -``` - -### C. Wider Sidebars - -**File:** `packages/core/src/browser/shell/application-shell.ts` - -```typescript -leftPanel: { - initialSizeRatio: 0.25 // 25% instead of 19.1% -}, -rightPanel: { - initialSizeRatio: 0.25 // 25% instead of 19.1% -} -``` - -### D. Smaller Bottom Panel - -```typescript -bottomPanel: { - initialSizeRatio: 0.30 // 30% instead of 38.2% -} -``` - ---- - -## 5. PRODUCT OS BRANDING - -### Custom Colors for Product OS - -```json -{ - "colors": { - // Google Cloud inspired - "activityBarBadge.background": "#4285F4", // Google Blue - "statusBar.background": "#34A853", // Google Green - "button.background": "#4285F4", - - // Dark background - "editor.background": "#121212", - "sideBar.background": "#1E1E1E", - "activityBar.background": "#0A0A0A", - } -} -``` - ---- - -## 🚀 Testing Your Changes - -### Development Workflow: - -1. **Edit design files** (CSS, JSON, TypeScript) -2. **Rebuild:** - ```bash - cd /Users/markhenderson/Cursor\ Projects/master-ai/theia - npm run build:electron - ``` -3. **Test:** - ```bash - npm run start:electron - ``` -4. **See changes immediately!** - -### Watch Mode (auto-rebuild): -```bash -npm run watch:electron -``` - ---- - -## 📁 File Structure Summary - -``` -theia/ -├── packages/ -│ ├── core/src/browser/ -│ │ ├── style/ -│ │ │ └── index.css ← Typography, spacing, borders -│ │ └── shell/ -│ │ └── application-shell.ts ← Panel sizes, layout structure -│ └── monaco/data/monaco-themes/vscode/ -│ └── dark_vs.json ← All colors -└── examples/electron/ - ├── package.json ← App name, branding - └── resources/ ← Icon, splash screen -``` - ---- - -## 🎯 Next Steps for Product OS Design - -1. **Lock dark theme** (remove light theme option) -2. **Custom color palette** (Google Cloud colors) -3. **Simplify UI** (hide developer-focused elements) -4. **Custom panels** (replace File Explorer with Product sections) -5. **Branding** (logo, splash screen, app icon) - ---- - -## 💡 Pro Tips - -- **CSS Variables** are your friend - change one variable, affects everywhere -- **Hot reload:** Changes to CSS show immediately with watch mode -- **Theme files:** JSON files control all colors - easy to swap themes -- **Test often:** Build and test frequently to see visual changes diff --git a/VIBN_PRD.md b/VIBN_PRD.md new file mode 100644 index 0000000..970f95f --- /dev/null +++ b/VIBN_PRD.md @@ -0,0 +1,501 @@ +# vibn — Product Requirements Document + +**Version:** 1.0 +**Date:** March 2026 +**Author:** Mark Henderson / Atlas AI +**Status:** Draft + +--- + +## 1. Executive Summary + +vibn is a template-first SaaS product builder for non-technical founders. It turns a product idea into a fully deployed, live web application — without writing code. Users describe their idea through a guided 6-phase wizard (Discover → Architect → Design → Market → Build), and vibn's AI agents scaffold, build, and deploy the product onto the user's own self-hosted infrastructure (Gitea + Coolify). vibn is positioned as "Shopify for building software": opinionated, template-driven, and designed to dramatically reduce failure rates compared to blank-page AI coding tools. The target customer is a non-technical or low-technical founder who has a validated idea and wants to get to a live product and first paying user in under 72 hours. + +--- + +## 2. Problem Statement + +**The problem:** Non-technical founders cannot build software products without hiring developers or becoming one themselves. Existing AI coding tools (Cursor, Replit, v0) assume technical literacy. General-purpose AI (ChatGPT) produces code snippets that can't be deployed. Developer agencies cost $50–200k and take 6–12 months. The gap between "I have a great idea" and "I have a live product" remains enormous. + +**Who experiences it:** Solo founders, domain experts (lawyers, trainers, consultants, operators) who want to productize a service, career changers, and micro-agencies wanting to scale client delivery without headcount. + +**What they do today instead:** +- Hire a freelance developer (slow, expensive, dependency risk) +- Use no-code tools like Bubble or Webflow (limited, technical ceiling, hard to customize) +- Try to learn to code (fails 90%+ of the time for non-native coders) +- Sit on the idea indefinitely + +**Why current alternatives fall short:** +- Bubble/Webflow: Hit a wall as soon as real backend logic is needed; proprietary and not portable +- AI coding tools: Require knowing what to ask, how to debug, how to deploy — the hard parts remain +- Agencies: Take too long, cost too much, and the founder loses control +- Hiring: Creates single-point-of-failure dependency + +--- + +## 3. Vision & Success Metrics + +**Vision:** vibn is the fastest path from idea to live product for anyone who can describe what they want. It removes every technical barrier between a non-technical founder and a running SaaS — planning, building, deploying, and marketing — while keeping the user in control and the infrastructure on their own servers. + +**Success metrics (v1, 6-month targets):** + +| Metric | Target | +|---|---| +| Time from signup to deployed app | < 72 hours (median) | +| % of builds that deploy successfully on first attempt | > 85% | +| Monthly active builders | 500 | +| Projects reaching "live" status | 200 | +| Net Revenue Retention (NRR) | > 100% | +| Gross margin | > 65% | +| Paying customers at 6 months | 150 | + +**Key milestones:** +- Month 1: Private beta with 10 hand-selected founders +- Month 2: 50 projects initiated, first 20 live +- Month 3: Public waitlist open, payment enabled +- Month 6: Self-serve onboarding, 150 paying customers + +--- + +## 4. Target Users & Personas + +### Persona A — The Non-Technical Founder ("The Builder") +- **Who:** A domain expert (ex: fitness coach, lawyer, ops manager) who has identified a software problem in their industry. No coding background. Has validated the idea informally with peers. +- **Primary goal:** Go from idea to a working product they can show to real users and start charging for. +- **Pain points:** Doesn't know where to start technically; has been burned by developers before; doesn't trust no-code tools for "real" products; overwhelmed by choices. +- **Happy path:** Describes idea in the Discover phase → reviews and approves architecture → picks a visual style → sets brand voice → hits "Build" → shares a live URL within 48 hours. +- **What they value:** Speed, control, clarity. They want to see something real, not a mock. + +### Persona B — The Micro-Agency Operator ("The Producer") +- **Who:** A freelancer or small agency (1–5 people) that builds web products for clients. Currently using developers or outsourcing. Wants to deliver faster and at higher margin. +- **Primary goal:** Build client products in days, not months. Manage multiple projects from one dashboard. Bill clients for AI compute costs with markup. +- **Pain points:** Hiring developers is expensive and slow. Coordinating freelancers is painful. Margins are thin. Can't take on more work without more headcount. +- **Happy path:** Creates a new client project → walks through wizard on behalf of client → client reviews and approves → vibn builds and deploys → operator bills client with AI cost markup shown. +- **What they value:** Speed, multi-project management, billing visibility, client-presentable output. + +### Permissions Matrix + +| Capability | Builder (own project) | Producer (client project) | +|---|---|---| +| Create project | ✓ | ✓ | +| Run wizard phases | ✓ | ✓ | +| Trigger build | ✓ | ✓ | +| View live app URL | ✓ | ✓ | +| View cost breakdown | Own costs only | Full client cost breakdown | +| Bill client | — | ✓ | +| Manage custom domain | ✓ | ✓ | +| Access Gitea repo | ✓ | ✓ | +| Request changes post-launch | ✓ | ✓ | + +--- + +## 5. User Flows & Journeys + +### Primary Flow — New Builder (Non-Technical Founder) + +1. Lands on vibn marketing site (`vibn.app`) +2. Clicks "Get started free" → enters email +3. Completes **Welcome phase**: sees 5-step overview of what vibn does, clicks "Let's build it" +4. **Discover phase**: guided 6-question chat conversation — idea, problem, users, value, revenue, features. Sees live PRD panel filling in as they answer. Continues when all 6 answered. +5. **Architect phase**: Reviews AI-generated architecture (frontend, backend, auth, payments, email, hosting). Each block shows the chosen option and why. Can edit any block. Confirms with "Plan looks good — next: Design". +6. **Design phase**: Picks visual feel from 6 presets (Clean, Bold, Warm, Fresh, Electric, Luxury). Sees live mock of their app updating in real time. +7. **Market phase**: Sets brand voice (sliders for tone, style, personality). Reviews and edits 3 AI-generated content topics. Previews their marketing website style. +8. **Build phase**: Reviews full summary (auth, payments, email, style, website, topics, pages). Clicks "Build my MVP". Watches 12-step live build progress. Receives live URL + Gitea repo link. +9. Redirected to **Dashboard** — sees project as "Live" with URL, stats, and action buttons. + +### Secondary Flow — Returning User (Dashboard → Change Request) + +1. Logs in → lands on Dashboard (projects screen) +2. Selects an existing project → clicks "Build" or "Grow" +3. Enters the relevant phase of the wizard in edit mode +4. Makes changes → re-triggers partial build +5. Returns to Dashboard, sees updated deployment + +### Secondary Flow — Agency Producer (Client Project) + +1. Logs in → clicks "+ New project" +2. Tags project as "Client" and enters client name +3. Walks through wizard as normal (can be done with client present or on their behalf) +4. After build: sees project card with "Client" tag, cost breakdown, and "Bill →" button +5. Clicks "Bill →" → generates itemized invoice (LLM costs + compute + markup) +6. Views unbilled total across all clients in Billing screen + +### Onboarding Flow + +1. Email signup → verify email +2. Welcome wizard (Welcome phase of builder) +3. First project created automatically — user is never left on an empty dashboard +4. If user exits mid-wizard, project is saved as draft and resumed on next login + +### Error / Recovery Flows + +- **Build fails mid-way:** User sees which step failed, error plain-English explanation, and "Retry" button. Failed build does not charge full credits. +- **Payment setup missing:** If user chose Stripe billing in Architect but hasn't connected Stripe, they're prompted before Build is triggered. +- **Custom domain fails DNS:** In-app guide walks through DNS setup; app is still live on vibn subdomain in the meantime. +- **User exits mid-wizard:** Progress is auto-saved per phase. Resumable from Dashboard. + +--- + +## 6. Feature Requirements + +### 6.1 Must Have (v1 Launch) + +**Builder Wizard — 6-Phase Flow** +- *Description:* The core product experience. A sequential, guided wizard that takes a user from idea to deployed product. +- *User story:* As a non-technical founder, I want to answer plain-English questions and have AI figure out the architecture, code, and deployment — so I never have to think about technical choices. +- *Acceptance criteria:* All 6 phases completable end-to-end. Progress saved between sessions. Each phase produces a visible artifact (PRD, architecture plan, design preview, etc.). + +**Discover Phase — Conversational PRD Builder** +- *Description:* 6-question guided chat. Each answer populates a live PRD panel. AI synthesizes answers into a structured product plan. +- *Acceptance criteria:* All 6 questions answered before proceeding. PRD panel shows structured output per question. "Plan looks good" CTA advances to next phase. + +**Architect Phase — Architecture Selection** +- *Description:* AI proposes 6 architecture blocks (Frontend, Backend, Auth, Payments, Email, Hosting). Each block is explainable in plain English and editable. +- *Acceptance criteria:* All 6 blocks shown with default selection and rationale. User can change any block via dropdown/modal. Hosting block is locked to self-hosted (Coolify + Gitea). Pages list shown. + +**Design Phase — Visual Feel Picker** +- *Description:* 6 visual presets. Selecting a preset updates a live app mock in real time. +- *Acceptance criteria:* 6 presets rendered correctly. Live mock updates within 300ms of selection. Continue CTA available once selection made. + +**Market Phase — Voice + Topics + Website** +- *Description:* Brand voice sliders (tone, style, personality). AI-generated content topics (add/edit/remove). Website style picker with live preview. +- *Acceptance criteria:* Voice sliders affect AI content generation downstream. Topics editable with add/remove. Website preview updates with style selection. + +**Build Phase — Review + Deploy** +- *Description:* Full summary of all decisions. "Build my MVP" button triggers 12-step build pipeline. Live progress shown. On completion: app URL + Gitea link. +- *Acceptance criteria:* All decisions shown accurately from prior phases. Build progress shows step-by-step status. On success: live URL displayed and functional. On failure: clear error + retry option. + +**Dashboard — Projects View** +- *Description:* Home screen after login. Shows all projects with status, basic stats, and actions. +- *Acceptance criteria:* Projects shown as cards with status (Live/Building), URL, and key stats (visitors, signups, MRR). "Continue building" for in-progress builds. "+ New project" creates a new wizard session. + +**Dashboard — Billing View (Agency)** +- *Description:* Client billing tab showing unbilled costs by client, LLM/compute/other breakdown, invoice generation. +- *Acceptance criteria:* Unbilled totals accurate. "Bill →" generates invoice. Cost log shows itemized charges. + +**Authentication** +- *Description:* Email-based signup/login for the vibn platform itself. +- *Acceptance criteria:* Email + password signup. Email verification required. Forgot password flow. Session persists across browser restarts. + +**Deployment Integration (Coolify + Gitea)** +- *Description:* Every built project is pushed to user's Gitea repo and deployed via Coolify automatically. +- *Acceptance criteria:* Gitea repo created on build start. Code committed on completion. Coolify deploy triggered automatically. App live on `[project].vibn.app` subdomain. + +**Floating AI Chat (Assist)** +- *Description:* Phase-aware chat assistant available throughout the builder wizard. Persists across phase navigation. +- *Acceptance criteria:* Chat available from Discover through Build phases. Phase-specific starter suggestions. Chat history persists across phase changes. Does not reset on navigation. + +--- + +### 6.2 Should Have (Fast Follow — Months 2–3) + +**Custom Domain Support** +- Users can connect their own domain to a deployed project. +- In-app DNS setup guide. SSL auto-provisioned via Coolify. + +**Post-Build Change Requests** +- Users can request changes to their live product in plain English. +- AI interprets, diffs the codebase, applies change, redeploys. + +**Marketing Autopilot** +- AI generates and schedules blog posts, email newsletters, and social content based on topics defined in Market phase. +- Initial manual approval required; can be set to auto-publish. + +**Credit Usage Display** +- Show real-time credit consumption during builds. +- Warn before triggering tasks estimated to cost > X credits. +- User-configurable spending cap per project. + +**Template Marketplace Access** +- Starter templates browsable before creating a project. +- Template selection sets pre-configured architecture defaults. + +--- + +### 6.3 Could Have (Future — Months 4–6) + +**Client-Facing Project Portal** +- Agency clients can log in to review progress, approve phases, and view their live app — without accessing the vibn dashboard directly. + +**Stripe Connect for Invoice Payment** +- Agency operators can receive payment from clients directly via vibn. + +**Analytics Dashboard (per project)** +- Built-in lightweight analytics (page views, signups, MRR) sourced from the deployed app's database. + +**Invite Team Members** +- Multiple vibn users can collaborate on a single project. + +**Mobile App (iOS/Android)** +- Native app for monitoring live projects and approving content scheduled by marketing autopilot. + +**Template Marketplace (Sell/Buy)** +- Third-party developers can submit templates; users can purchase premium templates. + +--- + +### 6.4 Explicitly Out of Scope (v1) + +| Feature | Reason excluded | +|---|---| +| Mobile app (iOS/Android) builder output | All v1 builds are web apps; native app generation is a later capability | +| Real-time multi-user collaboration on wizard | Single-user flow only in v1; collaboration is v2 | +| Self-hosting vibn itself (white-label) | Not offered in v1; Enterprise tier future consideration | +| AI voice/video generation | Out of scope; vibn generates text and code only | +| Direct Stripe Connect marketplace | Invoice workflow is manual export only in v1 | +| Custom AI model selection by users | Model routing is automatic; users do not choose models | +| Offline/desktop app | Web-only | +| HIPAA / SOC2 compliance | Out of scope for v1; required before any healthcare customers | + +--- + +## 7. Screen-by-Screen Specification + +### 7.1 Marketing Website (`vibn.app`) +- **Purpose:** Acquire non-technical founders. Convert to "Get started free" or "Log in". +- **Key elements:** Hero headline ("You have the idea. We handle everything else."), 5-step how-it-works, pull quotes from 3 founders, stats bar (280+ launched, 72h avg, 4.9 rating), empathy section, final CTA. +- **Actions:** Get started free → Welcome wizard. Log in → Dashboard. +- **Notes:** Lora serif + Inter sans, ink/parchment palette. No color accents. + +### 7.2 Welcome Phase +- **Purpose:** Orient the user, set expectations, build confidence. +- **Key elements:** 5-step overview of the vibn process. "Let's build it →" CTA. Tagline: "From idea to live product. No code needed." +- **Actions:** "Let's build it" → Discover phase. + +### 7.3 Builder Sidebar (phases 2–6) +- **Purpose:** Persistent navigation and progress tracking during the wizard. +- **Key elements:** vibn logo. Progress checklist (Product plan, Architecture, Product design, Marketing). Phase nav (Discover, Architect, Design, Market, Build MVP). User avatar + name + plan at bottom. +- **Notes:** Sidebar is hidden on Welcome and Website screens. Always visible during builder phases. + +### 7.4 Discover Phase +- **Purpose:** Capture the product idea as structured data. Output: PRD. +- **Key elements (left panel):** Phase header, progress bar across 6 questions, AI message bubble per question, user input field. +- **Key elements (right panel):** "Your Product Plan" — live-updating sections: Idea, Problem, Users, Value, Revenue, Features. Each fills in as answered. +- **Actions:** User types answers. AI asks follow-up. After 6 questions: "Plan looks good — next: Architect →" CTA. + +### 7.5 Architect Phase +- **Purpose:** Let user review and confirm the technical architecture in plain English. +- **Key elements (center):** Phase header. 6 architecture blocks as horizontal-scrollable cards (Frontend, Backend, Auth, Payments, Email, Hosting). Each card shows: icon, chosen option, plain-English explanation, "Change →" button. "Why?" expandable for each block. Infra note (Coolify + Gitea). +- **Key elements (right panel):** "Pages to Build" — grouped by Public, Auth, App, Payments. +- **Actions:** "Change →" opens selection modal with 2–4 alternatives per block. "Confirm — next: Design →" CTA. + +### 7.6 Design Phase +- **Purpose:** Choose a visual style for the product. +- **Key elements (left):** 6 feel cards (Clean, Bold, Warm, Fresh, Electric, Luxury) — each with label, reference product, and color/style preview. +- **Key elements (right):** Live app mock that updates to reflect selected feel. Shows a plausible dashboard UI in that style. +- **Actions:** Click a feel card → mock updates. "Next: Market →" CTA. + +### 7.7 Market Phase — Voice Tab +- **Purpose:** Set the brand voice for AI-generated content. +- **Key elements:** 3 slider pairs: Tone (Friendly ↔ Professional), Style (Conversational ↔ Precise), Personality (Warm ↔ Direct). "Voice preview" section shows how the brand would introduce itself. +- **Actions:** Sliders adjust in real time. Tab switches to Topics or Website. + +### 7.8 Market Phase — Topics Tab +- **Purpose:** Define the content topics AI will generate and publish. +- **Key elements:** 3 pre-generated topic cards (title, angle, channels). Each editable. "Add topic" button. Remove button per card. +- **Actions:** Edit, add, remove topics. "Next: Website →" tab. + +### 7.9 Market Phase — Website Tab +- **Purpose:** Choose the marketing website visual style. +- **Key elements:** 4 website style options (Editorial, Startup Energy, Ultra Minimal, Warm & Human). Live website preview panel updates on selection. +- **Actions:** Click style → preview updates. "Plan looks good — next: Build →" CTA. + +### 7.10 Build Phase — Review Screen +- **Purpose:** Final review before triggering the build. +- **Key elements:** Summary grid (Auth, Payments, Email, Product Style, Website Style, Campaign Topics). Pages list (by group). Infra deployment note. "▲ Build my MVP" button. Disclaimer: ~15 minutes, refinable after launch. +- **Actions:** "Build my MVP" → transitions to Build Progress screen. + +### 7.11 Build Phase — Progress Screen +- **Purpose:** Show real-time build progress. +- **Key elements:** 12-step checklist with: completed steps (green checkmark), active step (animated indicator), pending steps (grey). Step label + detail line. Progress header showing step count. +- **On completion:** "Your MVP is live" screen — app URL ("Open my app ↗"), Gitea link ("View in Gitea ↗"), "Your next 3 actions" card. + +### 7.12 Dashboard — Projects Screen +- **Purpose:** Manage all projects from one place. +- **Key elements:** "Your projects" header with count. Unbilled total button (if agency projects exist). "+ New project" button. Project cards (2-column grid): status thumbnail, project identity (name, URL, client if applicable), status pill (Live/Building), cost strip (client projects), stats (visitors, signups, MRR), action buttons (Build, Grow, ↗). New project CTA card (dashed border, "+" icon). +- **Activity feed:** Recent events across all projects (content published, new signups, build events). + +### 7.13 Dashboard — Billing Screen (Client Billing tab) +- **Purpose:** Manage invoicing for agency operators. +- **Key elements:** Summary stats (total unbilled, LLM costs, compute, other). Billing table (by client, by month). Each row: project, LLM, compute, other, total, status pill. "Invoice" button per unbilled row. "Generate invoice" button (global). + +### 7.14 Dashboard — Billing Screen (Cost Tracker tab) +- **Purpose:** Understand AI and infrastructure cost breakdown. +- **Key elements:** LLM usage breakdown (code gen, content, chat assist) with bar charts. Infrastructure breakdown (hosting, database, email, domain). Recent charges log (time, description, project, cost). + +### 7.15 Floating AI Chat (Assist) +- **Purpose:** On-demand AI help throughout the wizard. +- **Key elements:** Dark header with "Assist · [phase]" + live green dot. Message thread (user + assistant bubbles). Phase-specific starter suggestions (3 clickable). Input field + send button. +- **Behavior:** Persists open/closed state and message history across phase changes. Accessible via 💬 bubble button at bottom right. + +--- + +## 8. Business Model & Pricing + +### Revenue Model +**Subscription + Credits** (not unlimited AI) + +The subscription covers fixed platform value (infrastructure orchestration, templates, UX, dashboard, Gitea/Coolify integration, team ops). Credits cover variable AI compute costs (LLM calls across Tier A/B/C, build pipelines, content generation). + +### Pricing Tiers + +| Tier | Price | Templates | Projects | Credits included | Target | +|---|---|---|---|---|---| +| **Free** | $0/mo | Starter only | 1 active | 50 credits/mo | Evaluators | +| **Builder** | $49/mo | Starter + Builder | 3 active | 500 credits/mo | Solo founders | +| **Pro** | $149/mo | All templates | Unlimited | 2,000 credits/mo | Active builders + agencies | +| **Enterprise** | Custom | Custom + private | Unlimited | Custom | Teams, compliance needs | + +**Credit top-ups:** Available at $0.10/credit (10 credits = $1). Minimum top-up: $10. + +### AI Cost Structure (Internal) + +Three-tier model routing: +- **Tier A (40% of calls):** Gemini Flash-class — orchestration, summaries, routing, log parsing. ~$0.0001/1k tokens. +- **Tier B (45% of calls):** Mid-tier coding model (GLM-5 or Qwen Coder via Vertex) — code gen, feature building, refactors. ~$0.002/1k tokens. +- **Tier C (15% of calls):** Premium escalation (Claude Sonnet or Gemini Pro) — architecture decisions, high-risk changes, repeated failures. ~$0.015/1k tokens. + +**Credit pricing:** Each credit = approximately $0.10 of platform value (AI + margin). Exact credit cost per action surfaced to user before triggering high-cost tasks. + +### Cost Estimate Per Build (v1 template-based app) +| Item | Estimated cost | +|---|---| +| Discover/Architect/Design/Market phases (Tier A/B) | ~$0.80 | +| Full code generation (Tier B, ~8,000 LOC) | ~$2.40 | +| Deployment orchestration | ~$0.20 | +| **Total per build** | **~$3.40** | +| **Charged at markup** | **~40 credits ($4.00)** | + +At $49/mo (500 credits), a Builder subscriber can complete ~12 full builds per month within plan. + +--- + +## 9. Integrations & External Dependencies + +| Integration | Purpose | Notes | +|---|---|---| +| **Gitea (self-hosted)** | Code storage and version control for every built project | Required. All repos pushed here on build completion. | +| **Coolify (self-hosted)** | Build pipeline, deployment, container orchestration | Required. Auto-deploys on Gitea push. | +| **Google Vertex AI** | Tier A/B/C model calls | Primary AI provider. Gemini Flash (A), mid-tier MaaS (B), Claude/Gemini Pro (C). | +| **Stripe** | Subscription billing for vibn platform fees | Customers pay vibn via Stripe. Stripe not required in built apps unless user selects it in Architect. | +| **Resend / Postmark** | Transactional emails (signup, password reset, notifications) | For vibn platform emails. Built apps may use same if email selected in Architect. | +| **PostgreSQL** | Platform database (conversations, project state, tasks, billing) | Self-hosted in hot tier. | +| **Redis** | Job queue, pubsub for build pipeline events | Optional but recommended for build reliability. | + +**No external data import requirements in v1.** Built apps start fresh; no migration tooling in scope. + +--- + +## 10. Non-Functional Requirements + +### Performance +- Wizard phase transitions: < 200ms +- Live design mock updates: < 300ms after style selection +- Build pipeline: Median < 15 minutes for a template-based app +- Dashboard load: < 1 second (projects list) +- AI chat response: First token within 1 second + +### Platform +- **Primary:** Web (desktop browser) — Chrome, Safari, Firefox, Edge +- **Secondary:** Responsive mobile web for dashboard viewing (not wizard) +- **Not in scope v1:** Native iOS/Android apps + +### Accessibility +- WCAG 2.1 AA compliance for all interactive elements +- Keyboard navigable wizard phases +- Sufficient color contrast across all design tokens (ink on paper palette passes AA) + +### Compliance & Regulatory +- **GDPR:** Data processing agreements available for EU users. User data deletable on request. +- **PCI DSS:** vibn does not store card data; handled entirely by Stripe. +- **HIPAA:** Out of scope for v1. No healthcare data processed. +- **SOC 2:** Target for Enterprise tier; not required at launch. + +### Data Privacy & Security +- All user project code stored in user's own Gitea instance (user owns their data) +- vibn platform database stores: conversation history, project metadata, billing records +- AI conversations not used for model training (Vertex API terms) +- Secrets (API keys, Stripe keys) stored encrypted, never logged +- Build logs retained for 30 days, then purged + +### Scalability Assumptions (v1) +- Designed for 500 MAU at launch +- Build pipeline: 20 concurrent builds supported +- Horizontal scaling of worker pool via Coolify + +--- + +## 11. Risks & Mitigations + +| Risk | Likelihood | Impact | Mitigation | +|---|---|---|---| +| Build success rate < 85% due to AI code quality | Medium | High | Template-first architecture dramatically reduces open-ended generation. Fallback retry mechanism. Tiered escalation to better model on repeated failure. | +| LLM costs exceed credit pricing margins | Medium | High | 3-tier routing keeps 85% of calls on cheap models. Per-step token limits. Aggressive context summarization. Max retries cap (3). | +| Users don't understand "credits" model | High | Medium | In-app cost estimation before every build. Plain-English explanations. "This build will use ~40 credits." Spending caps user-configurable. | +| Coolify/Gitea self-hosted infra reliability | Low | High | Hot tier always-on. Healthcheck monitoring. Auto-restart policies. Graceful failure messaging in build UI. | +| Non-technical users abandon wizard mid-way | High | Medium | Progress auto-saved per phase. Resume from dashboard. Floating AI chat for unblocking. Encourage "good enough" answers — no wrong answers in Discover. | +| Scope creep in wizard phases | Medium | Medium | Each phase has a strict set of decisions. No free-form architecture input. Locked hosting block prevents deviation. | +| Competition from Replit, Bolt, v0 | High | Medium | Differentiator is self-hosted infra (user owns everything), template-first (higher success rate), and the end-to-end wizard (no coding literacy required). | +| Agency use case underperforms | Low | Low | Agency (Producer persona) is v1 secondary target. Builder persona is primary. Billing screen can be iterated post-launch. | + +--- + +## 12. Open Questions & Assumptions + +### Open Questions + +1. **Template library scope at launch:** How many starter templates exist at v1 launch? What are they? (Minimum: SaaS CRUD + landing page. What else?) +2. **Subdomain structure:** Are projects deployed to `[project-name].vibn.app` or `[user-slug]-[project].vibn.app`? (Collision risk if single namespace.) +3. **Build pipeline timing:** Is 15-minute median build time achievable for first template? What's the P95? +4. **Gitea/Coolify provisioning:** Is each user getting their own Gitea org? How are Coolify environments namespaced per user? +5. **Free tier limits:** Should free tier require a credit card? (Conversion vs. abuse risk tradeoff.) +6. **Change requests post-launch:** How are iterative changes billed? Per-change credit cost, or separate workflow? +7. **Marketing autopilot publishing:** In v1, does AI content require manual approval before publishing, or is auto-publish available? +8. **Wizard re-entry:** Can a user go back and redo an earlier phase after completing Build? Does this trigger a rebuild? + +### Assumptions Made + +- vibn's Gitea and Coolify infrastructure are already operational and stable before v1 user onboarding begins. +- Template-based builds (vs. blank-page builds) keep success rates above 85%. +- Non-technical founders are willing to pay $49–$149/month for a solution that reliably delivers a live product. +- The 6-phase wizard is completable in one sitting (~20–30 minutes) for a user with a clear idea. +- Vertex AI API access and model availability (Gemini Flash, mid-tier MaaS) is stable and within budget. +- Users do not need to understand or manage their Gitea/Coolify infrastructure directly — vibn abstracts it entirely. +- The primary acquisition channel for v1 is content marketing and founder communities (not paid ads). + +--- + +## 13. Appendix + +### Glossary + +| Term | Definition | +|---|---| +| **Build** | The automated process of AI generating code, committing to Gitea, and deploying via Coolify | +| **Wizard** | The 6-phase guided flow: Discover → Architect → Design → Market → Build | +| **Phase** | A single stage of the wizard, each producing a specific artifact | +| **Template** | A pre-built starter codebase that vibn AI builds upon instead of generating from scratch | +| **Credits** | vibn's unit of AI compute consumption; consumed during builds, content generation, and chat | +| **Hot tier** | Always-running shared infrastructure (API gateway, orchestrator, Postgres, Redis, Gitea, Coolify) | +| **Cold tier** | Per-user on-demand containers (agent workspace instances, hibernated when inactive) | +| **Tier A/B/C** | Three levels of AI model quality/cost, automatically routed by the orchestrator based on task complexity | +| **Producer** | A vibn user building products for clients (agency use case) | +| **Builder** | A vibn user building a product for themselves (founder use case) | +| **PRD** | Product Requirements Document — the structured output of the Discover phase | +| **Gitea** | Self-hosted open-source Git service; stores all project codebases | +| **Coolify** | Self-hosted deployment platform; builds and runs all deployed apps | + +### Reference Materials +- Product strategy document: `product-idea-a.md` +- Builder wizard UI prototype: `preview-assist-ui/src/App.jsx` +- Marketing website prototype: `preview-assist-ui/src/Website.jsx` +- Dashboard prototype: `preview-assist-ui/src/Dashboard.jsx` +- PRD agent system prompt: `prd-agent-prompt.pdf` + +### Competitor Reference +- **Bolt.new / Lovable:** AI coding from scratch; no deployment, no templates, requires iteration by user +- **Replit:** Strong coding environment; technical literacy required; no guided wizard +- **Webflow:** No-code UI builder; no real backend; visual but limited +- **Bubble:** No-code with backend; steep learning curve; proprietary lock-in +- **v0 (Vercel):** UI generation only; no deployment, no product planning +- **Agencies:** Custom development; 6–12 month timelines; $50k–$200k budgets diff --git a/architecture.md b/architecture.md deleted file mode 100644 index 36c177c..0000000 --- a/architecture.md +++ /dev/null @@ -1,349 +0,0 @@ -# Vibn Architecture - -## Overview - -Every project gets a persistent AI brain (the Master Orchestrator) that runs 24/7 on shared infrastructure. It manages specialist agents that handle Coding, Marketing, Support, Monitoring, and Debugging autonomously. Users interact through three channels: mobile app, browser dashboard, or the full Theia IDE. - ---- - -## System Layers - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ User Interfaces │ -│ │ -│ Mobile App Browser Dashboard Theia IDE │ -│ (chat + status) (chat + dashboards) (full IDE + │ -│ vibe coding) │ -│ REST/WebSocket REST/WebSocket WebSocket │ -└────────┬───────────────────┬───────────────────────┬────────────┘ - │ │ │ - └───────────────────┼───────────────────────┘ - │ -┌────────────────────────────┴────────────────────────────────────┐ -│ API Gateway │ -│ api.vibnai.com │ -│ Auth, routing, WebSocket upgrade, rate limiting │ -└────────────────────────────┬────────────────────────────────────┘ - │ -┌────────────────────────────┴────────────────────────────────────┐ -│ Master Orchestrator │ -│ (Hot Tier — always running) │ -│ │ -│ - Full project context (code, docs, marketing, support) │ -│ - Routes work to specialist agents │ -│ - Receives webhooks (Gitea push, Coolify deploy, support tix) │ -│ - Manages task queue across all agents │ -│ - Calls Gemini API │ -│ - Persists conversation history per project │ -│ │ -│ Specialist Agents (run as needed): │ -│ ┌──────────┬───────────┬──────────┬────────────┬───────────┐ │ -│ │ Coder │ Marketing │ Support │ Monitor │ Debugger │ │ -│ │ │ │ │ │ │ │ -│ │ Writes │ Updates │ Answers │ Watches │ Reads │ │ -│ │ code, │ landing │ user │ logs, │ errors, │ │ -│ │ tests, │ pages, │ tickets, │ uptime, │ traces │ │ -│ │ deploys │ docs, │ FAQ, │ alerts on │ stack, │ │ -│ │ via │ release │ drafts │ failures │ proposes │ │ -│ │ Gitea + │ notes │ replies │ │ fixes │ │ -│ │ Coolify │ │ │ │ │ │ -│ └──────────┴───────────┴──────────┴────────────┴───────────┘ │ -└────────────────────────────┬────────────────────────────────────┘ - │ -┌────────────────────────────┴────────────────────────────────────┐ -│ Shared Infrastructure │ -│ │ -│ Gitea (git.vibnai.com) — code, docs, marketing content │ -│ Coolify (coolify.vibnai.com) — deploys, hosting, logs │ -│ Database (Postgres) — conversations, tasks, user data │ -│ Object Storage — assets, screenshots, artifacts │ -└─────────────────────────────────────────────────────────────────┘ - - │ -┌────────────────────────────┴────────────────────────────────────┐ -│ User Workspaces (Cold Tier) │ -│ │ -│ Per-user Theia IDE containers │ -│ - Hibernate when not in use (storage persists) │ -│ - Wake in 2-5 seconds when user opens browser │ -│ - Mount project workspace volume │ -│ - Full code editor, terminal, AI chat │ -│ - For manual vibe coding sessions │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Master Orchestrator - -The central AI brain for each project. Always running on shared infrastructure. - -### What it knows -- Full codebase (via Gitea API) -- Deployment state (via Coolify API) -- Task/ticket history (database) -- Conversation history with the user across all channels -- What each specialist agent is doing - -### How it receives work -1. **User message** — from mobile app, browser, or Theia IDE chat -2. **Gitea webhook** — code pushed, PR opened, issue created -3. **Coolify webhook** — deploy completed, service crashed -4. **Scheduled checks** — cron-based monitoring, report generation -5. **Support ticket** — customer question routed in - -### How it dispatches -- Analyzes the event/message -- Decides which specialist agent(s) should act -- Dispatches to one or more agents simultaneously -- Tracks completion, reports results back to user -- Queues follow-up work if needed - ---- - -## Specialist Agents - -### Coder -- **Trigger**: User request, Orchestrator dispatch -- **Tools**: Gitea API (read/write code), Coolify API (deploy), shell execute -- **Output**: Code commits, PRs, deployments -- **Autonomy**: Can commit to feature branches, needs approval for main - -### Marketing -- **Trigger**: New feature deployed, Orchestrator dispatch -- **Tools**: Gitea API (update marketing repo), content templates -- **Output**: Updated landing pages, release notes, feature announcements -- **Autonomy**: Drafts content, queues for user review - -### Support -- **Trigger**: Support ticket/question received, Orchestrator dispatch -- **Tools**: Codebase search, docs search, conversation history -- **Output**: Draft replies, FAQ updates, escalation to Coder if it's a bug -- **Autonomy**: Can draft responses, user approves before sending - -### Monitor -- **Trigger**: Scheduled (every N minutes), Coolify webhook on failure -- **Tools**: Coolify API (logs, status), health check endpoints -- **Output**: Status reports, alerts, escalation to Debugger on failures -- **Autonomy**: Fully autonomous — monitors and alerts without user input - -### Debugger -- **Trigger**: Monitor detects failure, user reports bug, error logs -- **Tools**: Coolify logs, Gitea code search, stack trace analysis -- **Output**: Root cause analysis, proposed fix, delegates to Coder if approved -- **Autonomy**: Analyzes and proposes, needs approval to fix - ---- - -## User Interfaces - -### Mobile App (chat-first) -- Simple chat interface to the Master Orchestrator -- Push notifications for alerts, completed tasks, questions needing approval -- Quick actions: approve deploy, review draft, check status -- No code editing — just conversation and oversight - -### Browser Dashboard (command center) -- Chat with the Master Orchestrator -- Dashboard panels: deploy status, agent activity, recent changes -- Review queues: marketing drafts, support replies, code PRs -- Project timeline and task tracking -- No code editing — management and oversight - -### Theia IDE (full workspace) -- Full code editor, terminal, file tree -- AI chat with Coder agent for hands-on vibe coding -- Design panel for visual preview -- For when the user wants to write code themselves -- Connects to the same project workspace as the Orchestrator - ---- - -## Communication Flow - -### User chats from mobile -``` -User (mobile): "How's the launch looking?" - → API Gateway → Master Orchestrator - → Orchestrator checks: Coder status, Marketing status, Monitor status - → Response: "Code is deployed. Marketing page updated. - 99.8% uptime last 24h. Two support tickets pending your review." -``` - -### User assigns work from mobile -``` -User (mobile): "Add a pricing page with three tiers" - → Master Orchestrator dispatches: - 1. Coder: build /pricing route with tier components - 2. Marketing: draft copy for three pricing tiers - → User closes app - → Coder commits code, deploys to staging - → Marketing drafts copy, queues for review - → User gets push notification: "Pricing page ready for review" -``` - -### Automated monitoring -``` -Monitor agent (scheduled): checks Coolify every 5 min - → Detects: API response time > 2s - → Escalates to Debugger - → Debugger: reads recent commits, checks logs - → Debugger: "Memory leak in auth middleware introduced in commit abc123" - → Orchestrator: notifies user via push notification - → User (mobile): "Fix it" - → Orchestrator → Coder: revert/fix the commit, deploy -``` - ---- - -## Infrastructure - -### Hot Tier (shared, always running) -- 2-4 servers on Hetzner/Coolify -- Runs Master Orchestrator + specialist agents for ALL projects -- Stateless compute — reads/writes to Gitea and database -- Scales horizontally with demand -- Estimated cost: $60-200/mo for the first 1,000 projects - -### Cold Tier (per-user, on-demand) -- Theia IDE containers, one per user workspace -- Hibernate after idle timeout -- Wake on browser access -- Storage persists (workspace volumes) -- Estimated cost: $1-2/mo per user (mostly storage) - -### Shared Services -- **Gitea**: code, docs, marketing content (self-hosted on Coolify) -- **Coolify**: container orchestration, deploys, logs -- **Postgres**: conversations, tasks, user accounts, agent state -- **Redis** (optional): task queue, real-time pub/sub for agent coordination - ---- - -## Data Flow - -``` -All code/content → Gitea repos (source of truth) -All deploys → Coolify (hosting + logs) -All conversations → Postgres (history + context) -All agent state → Postgres (what's running, what's queued) -``` - -Every specialist agent reads from and writes to these shared services. -No agent has local state that would be lost on restart. -The Master Orchestrator coordinates but doesn't store — it queries. - ---- - -## Agent Tool Registry - -The agent runner uses a modular tool registry (`src/tools/`). Each domain file registers its tools on import — agents declare which subset they use. - -| Tool file | Tools | Used by | -|-----------|-------|---------| -| `file.ts` | file read/write/list | Orchestrator, Coder | -| `shell.ts` | shell execute | Orchestrator, Coder | -| `git.ts` | git operations | Orchestrator, Coder | -| `gitea.ts` | Gitea API (repos, issues, PRs) | Orchestrator, Coder | -| `coolify.ts` | Coolify API (deploy, logs, status) | Orchestrator, Monitor | -| `agent.ts` | spawn sub-agents | Orchestrator | -| `memory.ts` | knowledge base read/write | All agents | -| `skills.ts` | reusable markdown skill lookup | All agents | -| `prd.ts` | `finalize_prd` — save completed PRD | Atlas | -| `search.ts` | `web_search` — internet search via Jina AI | Atlas | - -### Web Search (`web_search` tool) - -Atlas has access to real-time web search via [Jina AI's search endpoint](https://s.jina.ai/) — completely free, no API key required. - -**How it works:** -- Atlas calls `web_search` with a plain-language query -- The tool fetches `https://s.jina.ai/` which returns clean markdown-formatted results -- Results are truncated to ~6,000 characters to keep context window usage reasonable -- Atlas uses this to ground discovery conversations in real-world context - -**What Atlas uses it for:** -- Researching competitors and existing solutions -- Understanding market pricing and business models -- Looking up relevant technologies, frameworks, or integrations the user mentions -- Validating assumptions ("is this a solved problem? what do incumbents miss?") - -**No configuration needed** — Jina AI's free tier requires no credentials. If stricter control or higher volume is needed in future, swap the endpoint for Tavily, Brave Search, or Google Custom Search by updating `src/tools/search.ts`. - ---- - -## Product Lifecycle (Current Design) - -### Correct sequence — not yet fully implemented - -``` -1. Create project (name only — no scaffold yet) - ↓ -2. Atlas discovery conversation - - Understands the product concept - - Determines product type (SaaS / Marketplace / E-commerce / AI / Website / Mobile) - - Determines required surfaces (Web App, Marketing Site, Admin, Mobile, Email, Docs) - - Determines design package per surface - - Generates PRD with all of the above as structured data - ↓ -3. Architect agent - - Reads PRD (product type, surfaces, design choices) - - Designs technical solution and data model - - Generates Gitea repo scaffold tailored to the specific surfaces needed - - NOT a generic Turborepo template — apps/ reflects exactly what was decided - ↓ -4. Builder / Orchestrator - - Reads PRD + architecture - - Builds the product surface by surface - ↓ -5. Active (Theia IDE + Orchestrator for ongoing work) -``` - -### What is built today (and needs to change) - -| Step | Current behaviour | Correct behaviour | -|------|------------------|-------------------| -| Project creation | Creates Gitea repo + generic Turborepo scaffold immediately | Create project record only — no repo yet | -| Atlas | Saves PRD markdown + sets stage to `architecture` | Also saves `productType` and `surfaces[]` as structured fields | -| Design page | Shows Turborepo `apps/` from Gitea | Reads `surfaces[]` from PRD, shows theme picker per surface | -| Architect | Not built yet | Reads PRD + surfaces, generates tailored Gitea scaffold | - -### Design surfaces and recommended libraries - -| Surface | When needed | Library options | -|---------|-------------|----------------| -| **Web App** | SaaS, Marketplace, AI Product | shadcn/ui, Mantine, HeroUI, Tremor | -| **Marketing Site** | Almost every product | DaisyUI, HeroUI, Aceternity UI, Tailwind only | -| **Admin / Internal** | SaaS, Marketplace, E-commerce | Mantine, shadcn/ui, Tremor | -| **Mobile App** | Mobile-first products | NativeWind, Gluestack, RN Paper | -| **Email** | SaaS, E-commerce, Marketplace | React Email + Resend | -| **Docs / Content** | Developer tools, complex products | Nextra, Starlight, Docusaurus | - ---- - -## What to build (in order) - -### Phase 1: Foundation -- [ ] Move AI agents to Theia backend (server-side execution) -- [ ] Master Orchestrator service with multi-agent dispatch -- [ ] Postgres schema for conversations, tasks, agent state -- [ ] API Gateway with auth and WebSocket support - -### Phase 2: Agents -- [ ] Coder agent (already exists — extract from Theia frontend) -- [ ] Monitor agent (Coolify log watcher + health checks) -- [ ] Marketing agent (content generation + Gitea commits) -- [ ] Support agent (ticket intake + draft responses) -- [ ] Debugger agent (log analysis + fix proposals) - -### Phase 3: Interfaces -- [ ] Browser dashboard (React app, chat + status panels) -- [ ] Mobile app (React Native or Flutter, chat + push notifications) -- [ ] Theia IDE integration (connect to Master Orchestrator) - -### Phase 4: Scale -- [ ] Workspace hibernation and wake-on-access -- [ ] Multi-project support per user -- [ ] Hot tier horizontal scaling -- [ ] Usage-based billing diff --git a/branding/coolify/README.md b/branding/coolify/README.md new file mode 100644 index 0000000..920dc31 --- /dev/null +++ b/branding/coolify/README.md @@ -0,0 +1,33 @@ +# Coolify White Label — Vibn Branding + +Users never see the Coolify UI directly (it's backend infrastructure), but if you +want the admin panel to match, add these to the Coolify `.env` on the server. + +## Steps + +```bash +# SSH into the server +gcloud compute ssh coolify-server-mtl --zone=northamerica-northeast1-a + +# Edit the Coolify env file +sudo nano /data/coolify/source/.env +``` + +## Add These Lines + +```env +COOLIFY_WHITE_LABELED=true +COOLIFY_WHITE_LABELED_ICON=https://vibnai.com/vibn-icon.png +``` + +The icon URL must be publicly accessible. Once your logo is live on vibnai.com, +point this to the 180×180 PNG version. + +## Apply Changes + +```bash +cd /data/coolify/source +docker compose down && docker compose up -d +``` + +Note: These settings are lost on Coolify upgrades — re-apply if you update Coolify. diff --git a/branding/gitea/conf/app.ini b/branding/gitea/conf/app.ini new file mode 100644 index 0000000..d04aa32 --- /dev/null +++ b/branding/gitea/conf/app.ini @@ -0,0 +1,10 @@ +; Vibn branding overrides for Gitea +; These settings go in $GITEA_CUSTOM/conf/app.ini +; On the server this is at: /data/gitea/data/gitea/custom/conf/app.ini + +[ui] +SITE_TITLE = Vibn + +[server] +; Optional: change the landing page description meta tag +LANDING_PAGE = home diff --git a/branding/gitea/public/assets/img/README.md b/branding/gitea/public/assets/img/README.md new file mode 100644 index 0000000..3fc2913 --- /dev/null +++ b/branding/gitea/public/assets/img/README.md @@ -0,0 +1,34 @@ +# Gitea Brand Assets — Drop Files Here + +Place the following files in this directory, then copy them to the server at: +`/data/gitea/data/gitea/custom/public/assets/img/` + +Then restart Gitea for changes to take effect. + +## Required Files + +| File | Format | Dimensions | Purpose | +|---|---|---|---| +| `logo.svg` | SVG | Vector | Main nav logo | +| `logo.png` | PNG | 512×512 | Open Graph previews | +| `favicon.svg` | SVG | Vector | Browser tab (primary) | +| `favicon.png` | PNG | 180×180 | Browser tab (fallback) | +| `apple-touch-icon.png` | PNG | 180×180 | iOS bookmark icon | +| `avatar_default.png` | PNG | 200×200 | Default user avatar | + +## How to Deploy + +```bash +# From local machine: +gcloud compute scp branding/gitea/public/assets/img/* \ + coolify-server-mtl:/data/gitea/data/gitea/custom/public/assets/img/ \ + --zone=northamerica-northeast1-a + +gcloud compute scp branding/gitea/conf/app.ini \ + coolify-server-mtl:/data/gitea/data/gitea/custom/conf/app.ini \ + --zone=northamerica-northeast1-a + +# Then restart Gitea: +gcloud compute ssh coolify-server-mtl --zone=northamerica-northeast1-a \ + --command="sudo docker restart gitea-bcc4k0kog0w4ckkskg8gwggc" +``` diff --git a/branding/ux-testing/01_homepage.restructured-green.html b/branding/ux-testing/01_homepage.restructured-green.html new file mode 100644 index 0000000..25e5446 --- /dev/null +++ b/branding/ux-testing/01_homepage.restructured-green.html @@ -0,0 +1,242 @@ + + + + +vibn — Homepage + + + + + + + + + +
+
+ + +
+
For non-technical founders
+

+ You have the idea.
We handle
everything else. +

+

You describe it. Vibn builds it, launches it, and markets it. From idea to live product in 72 hours — no code, no agencies, no waiting.

+
+ + +
+
+ + +
+
Your idea
+

"I want to build a booking tool for independent personal trainers."

+
+ ↵ Enter +
+
+ + +
+
vibn generated
+
+ +
+ Pages + Landing, Dashboard, Booking, Payments +
+
+ Stack + Auth, database, payments — handled +
+
+ Revenue + Subscription · $29 / mo +
+
+ Status + ⬤  Ready to build +
+ +
+
+ +
+
+ +
+ + +
+ + ★★★★★  280 founders launched +

No credit card required · Free forever plan

+ See how it works → +
+ +
+ + +
+
+
+
Sound familiar?
+

The idea is the hard part. Everything else shouldn't be.

+

You know exactly what you want to build and who it's for. But the moment you think about servers, databases, deployment pipelines, SEO — the whole thing stalls.

+

vibn exists to remove all of that. Not abstract it — remove it entirely.

+
+
+
No more "I need to hire a developer first"
vibn is your developer. Start building the moment you have an idea.
+
No more staring at a blank marketing calendar
AI generates and publishes your content every single week.
+
No more "I'll launch when it's ready"
Most founders ship their first version in under 72 hours.
+
+
+
+ + +
+
How it works
+

Four phases. One complete product.

+
+
01 — Discover
Define your idea

Six guided questions turn a rough idea into a full product plan — pages, architecture, revenue model. No jargon.

+
02 — Design
Choose your style

Pick a visual style and see your exact site and emails live before a single line of code is written.

+
03 — Build
Your app, live

AI writes every line. Auth, database, payments, all pages — deployed and live. Describe changes in plain English.

+
04 — Grow
Market & automate

AI generates your blog, emails, and social schedule — publishing on autopilot so you can focus on users.

+
+
+ + +
+
+ +
+
+
A live, working product
+

Not a prototype. Real auth, real payments, real database — on your own URL from day one.

+
+ +
+
+
A full marketing engine
+

Blog posts, onboarding emails, and social content — written and published automatically every week.

+
+ +
+
+
A product that evolves
+

Describe changes in plain English. Vibn handles the code so your product grows as fast as your ideas.

+
+ +
+
+ + +
+
+ + +
+ + +
+
+
+

"I had the idea for 2 years. The backend terrified me. vibn shipped it in 4 days and handles all my marketing."

+ — Alex K., founder of Taskly +
+
+ + +
+
+

"I have zero coding experience. Three weeks in, I have 300 paying users. That's entirely because of vibn."

+ — Marcus L., founder of Flowmatic +
+ + +
+
+
+

"The marketing autopilot saved me ten hours a week. My blog runs itself. I just focus on product."

+ — Sara R., founder of Nudge +
+
+ +
+ + +
+
+
+
+
+ +
+
+ + +
+
+
280+
founders launched
+
72h
average time to first version
+
4.9★
average rating
+
faster than hiring a developer
+
+
+ + +
+
+

Your idea deserves to exist.

+

Thousands of ideas never make it past a spreadsheet. Yours doesn't have to be one of them.

+ +
Joins 280+ non-technical founders already live
+
+
+ + + + + + diff --git a/branding/ux-testing/01_homepage.restructured.html b/branding/ux-testing/01_homepage.restructured.html new file mode 100644 index 0000000..3117192 --- /dev/null +++ b/branding/ux-testing/01_homepage.restructured.html @@ -0,0 +1,242 @@ + + + + +vibn — Homepage + + + + + + + + + +
+
+ + +
+
For non-technical founders
+

+ You have the idea.
We handle
everything else. +

+

You describe it. Vibn builds it, launches it, and markets it. From idea to live product in 72 hours — no code, no agencies, no waiting.

+
+ + +
+
+ + +
+
Your idea
+

"I want to build a booking tool for independent personal trainers."

+
+ ↵ Enter +
+
+ + +
+
vibn generated
+
+ +
+ Pages + Landing, Dashboard, Booking, Payments +
+
+ Stack + Auth, database, payments — handled +
+
+ Revenue + Subscription · $29 / mo +
+
+ Status + ⬤  Ready to build +
+ +
+
+ +
+
+ +
+ + +
+ + ★★★★★  280 founders launched +

No credit card required · Free forever plan

+ See how it works → +
+ +
+ + +
+
+
+
Sound familiar?
+

The idea is the hard part. Everything else shouldn't be.

+

You know exactly what you want to build and who it's for. But the moment you think about servers, databases, deployment pipelines, SEO — the whole thing stalls.

+

vibn exists to remove all of that. Not abstract it — remove it entirely.

+
+
+
No more "I need to hire a developer first"
vibn is your developer. Start building the moment you have an idea.
+
No more staring at a blank marketing calendar
AI generates and publishes your content every single week.
+
No more "I'll launch when it's ready"
Most founders ship their first version in under 72 hours.
+
+
+
+ + +
+
How it works
+

Four phases. One complete product.

+
+
01 — Discover
Define your idea

Six guided questions turn a rough idea into a full product plan — pages, architecture, revenue model. No jargon.

+
02 — Design
Choose your style

Pick a visual style and see your exact site and emails live before a single line of code is written.

+
03 — Build
Your app, live

AI writes every line. Auth, database, payments, all pages — deployed and live. Describe changes in plain English.

+
04 — Grow
Market & automate

AI generates your blog, emails, and social schedule — publishing on autopilot so you can focus on users.

+
+
+ + +
+
+ +
+
+
A live, working product
+

Not a prototype. Real auth, real payments, real database — on your own URL from day one.

+
+ +
+
+
A full marketing engine
+

Blog posts, onboarding emails, and social content — written and published automatically every week.

+
+ +
+
+
A product that evolves
+

Describe changes in plain English. Vibn handles the code so your product grows as fast as your ideas.

+
+ +
+
+ + +
+
+ + +
+ + +
+
+
+

"I had the idea for 2 years. The backend terrified me. vibn shipped it in 4 days and handles all my marketing."

+ — Alex K., founder of Taskly +
+
+ + +
+
+

"I have zero coding experience. Three weeks in, I have 300 paying users. That's entirely because of vibn."

+ — Marcus L., founder of Flowmatic +
+ + +
+
+
+

"The marketing autopilot saved me ten hours a week. My blog runs itself. I just focus on product."

+ — Sara R., founder of Nudge +
+
+ +
+ + +
+
+
+
+
+ +
+
+ + +
+
+
280+
founders launched
+
72h
average time to first version
+
4.9★
average rating
+
faster than hiring a developer
+
+
+ + +
+
+

Your idea deserves to exist.

+

Thousands of ideas never make it past a spreadsheet. Yours doesn't have to be one of them.

+ +
Joins 280+ non-technical founders already live
+
+
+ + + + + + diff --git a/flatten.sh b/flatten.sh new file mode 100644 index 0000000..8965209 --- /dev/null +++ b/flatten.sh @@ -0,0 +1,26 @@ +#!/bin/bash +cd "/Users/markhenderson/Cursor Projects/master-ai" + +# Check if nested master-ai exists +if [ -d "master-ai" ]; then + echo "Found nested master-ai folder, flattening..." + + # Move all contents from nested to temp folder + mv master-ai master-ai-nested + + # Move everything from nested up one level + mv master-ai-nested/* . + mv master-ai-nested/.git* . 2>/dev/null || true + + # Remove empty nested folder + rm -rf master-ai-nested + + echo "Flattened successfully!" +else + echo "No nested folder found, structure is clean" +fi + +# Show final structure +echo "" +echo "Final structure:" +ls -la | head -20 diff --git a/gitea-docker-compose.yml b/gitea-docker-compose.yml new file mode 100644 index 0000000..14139ef --- /dev/null +++ b/gitea-docker-compose.yml @@ -0,0 +1,41 @@ +version: '3.8' + +services: + gitea: + image: gitea/gitea:latest + container_name: gitea + restart: unless-stopped + ports: + - "3000:3000" + - "2222:22" + volumes: + - gitea_data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + environment: + - USER_UID=1000 + - USER_GID=1000 + - GITEA__server__ROOT_URL=https://git.vibnai.com + - GITEA__server__DOMAIN=git.vibnai.com + labels: + - "traefik.enable=true" + # HTTP router (will redirect to HTTPS) + - "traefik.http.routers.gitea-http.rule=Host(`git.vibnai.com`)" + - "traefik.http.routers.gitea-http.entrypoints=http" + - "traefik.http.routers.gitea-http.middlewares=redirect-to-https@docker" + # HTTPS router + - "traefik.http.routers.gitea-https.rule=Host(`git.vibnai.com`)" + - "traefik.http.routers.gitea-https.entrypoints=https" + - "traefik.http.routers.gitea-https.tls=true" + - "traefik.http.routers.gitea-https.tls.certresolver=letsencrypt" + # Service + - "traefik.http.services.gitea.loadbalancer.server.port=3000" + # Redirect middleware + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true" + # Coolify managed label (so Coolify doesn't ignore it) + - "coolify.managed=true" + +volumes: + gitea_data: + driver: local diff --git a/justine/00_design-tokens.css b/justine/00_design-tokens.css new file mode 100644 index 0000000..c2468f2 --- /dev/null +++ b/justine/00_design-tokens.css @@ -0,0 +1,26 @@ +/* vibn Design Tokens — Ink & Parchment */ +/* Import this in any HTML file or reference these values */ + +@import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;0,700;1,400;1,600&family=Inter:wght@400;500;600&display=swap'); + +:root { + --ink: #1a1510; + --ink2: #2c2c2a; + --ink3: #444441; + --mid: #5f5e5a; + --muted: #888780; + --stone: #b4b2a9; + --parch: #d3d1c7; + --cream: #f1efe8; + --paper: #f7f4ee; + --white: #fdfcfa; + --border: #e8e2d9; + + --serif: 'Lora', Georgia, serif; + --sans: 'Inter', sans-serif; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } +body { font-family: var(--sans); background: var(--paper); color: var(--ink); } +.f { font-family: var(--serif); } +.s { font-family: var(--sans); } diff --git a/justine/01_homepage.html b/justine/01_homepage.html new file mode 100644 index 0000000..f947a1d --- /dev/null +++ b/justine/01_homepage.html @@ -0,0 +1,335 @@ + + + + +vibn — Homepage + + + + + + + + + + + + +
+
+ + +
+
For non-technical founders
+

+ You have the idea.
We handle
everything else. +

+

You describe it. Vibn builds it, launches it, and markets it. From idea to live product in 72 hours — no code, no agencies, no waiting.

+
+ + +
+
+ + +
+
Your idea
+

"I want to build a booking tool for independent personal trainers."

+
+ ↵ Enter +
+
+ + +
+
vibn generated
+
+
+ Pages + Landing, Dashboard, Booking, Payments +
+
+ Stack + Auth, database, payments — handled +
+
+ Revenue + Subscription · $29 / mo +
+
+ Status + ⬤  Ready to build +
+
+
+ +
+
+ +
+ + +
+ +
★★★★★  280 founders launched
+

No credit card required · Free forever plan

+ See how it works → +
+ +
+ + +
+
+
+
+
Sound familiar?
+

The idea is the hard part. Everything else shouldn't be.

+

You know exactly what you want to build and who it's for. But the moment you think about servers, databases, deployment pipelines, SEO — the whole thing stalls.

+

vibn exists to remove all of that. Not abstract it — remove it entirely.

+
+
+
No more "I need to hire a developer first"
vibn is your developer. Start building the moment you have an idea.
+
No more staring at a blank marketing calendar
AI generates and publishes your content every single week.
+
No more "I'll launch when it's ready"
Most founders ship their first version in under 72 hours.
+
+
+
+
+ + +
+
How it works
+

Four phases. One complete product.

+
+
01 — Discover
Define your idea

Six guided questions turn a rough idea into a full product plan — pages, architecture, revenue model. No jargon.

+
02 — Design
Choose your style

Pick a visual style and see your exact site and emails live before a single line of code is written.

+
03 — Build
Your app, live

AI writes every line. Auth, database, payments, all pages — deployed and live. Describe changes in plain English.

+
04 — Grow
Market & automate

AI generates your blog, emails, and social schedule — publishing on autopilot so you can focus on users.

+
+
+ + +
+
+ +
+
+
A live, working product
+

Not a prototype. Real auth, real payments, real database — on your own URL from day one.

+

Runs on your own servers — your data, your infrastructure, no lock-in.

+
+ +
+
+
A full marketing engine
+

Blog posts, onboarding emails, and social content — written and published automatically every week.

+
+ +
+
+
A product that evolves
+

Describe changes in plain English. Vibn handles the code so your product grows as fast as your ideas.

+
+ +
+
+ + +
+
+ +
+ + +
+
+
+

"I had the idea for 2 years. The backend terrified me. vibn shipped it in 4 days and handles all my marketing."

+ — Alex K., founder of Taskly +
+
+ + +
+
+

"I have zero coding experience. Three weeks in, I have 300 paying users. That's entirely because of vibn."

+ — Marcus L., founder of Flowmatic +
+ + +
+
+
+

"The marketing autopilot saved me ten hours a week. My blog runs itself. I just focus on product."

+ — Sara R., founder of Nudge +
+
+ +
+ + +
+
+
+
+
+ +
+
+ + +
+
+
280+
founders launched
+
72h
average time to first version
+
4.9★
average rating
+
faster than hiring a developer
+
+
+ + +
+
+

Your idea deserves to exist.

+

Thousands of ideas never make it past a spreadsheet. Yours doesn't have to be one of them.

+ +
Joins 280+ non-technical founders already live
+
+
+ + + + + + + diff --git a/justine/02_signup.html b/justine/02_signup.html new file mode 100644 index 0000000..7b8f431 --- /dev/null +++ b/justine/02_signup.html @@ -0,0 +1,329 @@ + + + + +vibn — Sign up + + + + + + + +
+
+ + +
+
+
1
+ Account +
+
+
+
2
+ Your experience +
+
+
+
3
+ Ready +
+
+ + +
+

Let's build your first product.

+

Free to start · No credit card needed

+ + + + +
+
+ or continue with email +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+ + +

Joining 280+ founders already building

+ + +

By continuing you agree to our Terms and Privacy Policy

+
+
+ + + + + + + +
+
+ + + + diff --git a/justine/03_dashboard.html b/justine/03_dashboard.html new file mode 100644 index 0000000..c69cf25 --- /dev/null +++ b/justine/03_dashboard.html @@ -0,0 +1,1652 @@ + + + + +vibn — Dashboard + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + +
+
+

Good morning, Jane.

+

Here's what's happening across your products today.

+
+
+ + +
+
+ + + + + +
Portfolio snapshot
+
+
+
4
+
Active projects
+
+
+
2
+
Live products
+
+
+
1
+
Building now
+
+
+
$60.60
+
Unbilled revenue
+
+
+ + +
+
Overall performance
+ Aggregated across all projects +
+
+ +
+
Monthly Revenue
+
$1,050
+
↑ +$60 from last month
+
Launchpad $840 · Flowmatic $210
+
+ + + + + + +
+
+ +
+
Active Users
+
180
+
↑ +12 this week
+
Across 2 live products
+
+ + + + + + +
+
+ +
+
Build Progress
+
60%
+
↑ Taskly on track
+
Payment milestone due Mar 30
+
+
+
+
+
+ 3 of 5 milestones done + Due Mar 30 +
+
+
+ +
+ + +
Next milestones
+
+
+ + +
+
+
+
+
Payment integration
+
Taskly · Stripe checkout
+
+
+ In progress +
+
Mar 30
+
7 days left
+
+
+ + +
+
+
+
+
Launch & handoff
+
Taskly · Beta Labs delivery
+
+
+ Upcoming +
+
Mar 31
+
8 days left
+
+
+ + +
+
+
+
+
Reach 200 active users
+
Launchpad · Currently 142
+
+
+ On track +
+
Apr 15
+
23 days left
+
+
+ + +
+
+
+
+
Complete project setup
+
Notion Clone · Define & scope
+
+
+ Not started +
+
No date
+
+
+ +
+
+ + +
Helpful resources
+
+ +
+
📊
+
SaaS Metrics 101
+
Learn the key numbers every SaaS founder should track — MRR, churn, LTV, CAC, and what they actually mean for your business.
+ +
+ +
+
🚀
+
From idea to MVP
+
A step-by-step guide to turning a rough concept into a working product people will pay for — without overbuilding or wasting time.
+ +
+ +
+
💰
+
Pricing your SaaS
+
How to pick a pricing model, set your first price, and increase it over time — with real examples from indie makers who got it right.
+ +
+ +
+ +
+
+ + + +
+
+
+
+
+
L
+

Launchpad

+ Live + + + launchpad.vibn.app + +
+

AI-powered launch page builder for indie makers

+
+
+ + + +
+
+ +
+
📈
+
+
What to do next
+
Focus on conversion this week
+

You're pulling 2,400 visitors per month but only 6% convert to paying users. Improving your pricing page or onboarding flow could unlock significant growth — even a 2% lift adds $168/mo in MRR.

+ +
+
+ +
+
Monthly revenue
$840
↑ +$60 this month
+
Active users
142
3 new this week
+
Visitors / month
2,400
→ Stable
+
Conversion rate
8%
↑ from 6% last month
+
+ +
+
+
Site performance
+
+
Traffic is stable
2,400 visitors/mo — no drop detected
+
📈
MRR growing +7.7%
$780 → $840 vs last month
+
👥
3 new paying users
This week · 0 churn this month
+
📈
Conversion improving
8% vs 6% last month
+
+
+
+
Recent activity
+
+
Blog post published
"How to launch faster with AI"
2h ago
+
New signup
marcus@email.com
5h ago
+
Newsletter #12 scheduled
Sends tomorrow at 9am
Yesterday
+
+
+
+
Revenue summary
+
+
Monthly recurring revenue$840
+
Unbilled$0 — all clear
+
Billing typePersonal product
+
+
+
+
Recommendations
+
+
💡 Run an A/B test on pricing
Industry conversion for your category is 8–12%. At 6%, a better pricing page could add $100–200/mo.
+
📦 Consider annual pricing
An annual plan typically improves LTV by 30% and gives you better cash flow predictability.
+
+
+
+
+
+ + + +
+
+
+
+
+
F
+

Flowmatic

+ Live + Acme Corp + + + flowmatic.app + +
+

Workflow automation tool built for Acme Corp

+
+
+ + +
+
+ +
+
💰
+
+
Ready to bill
+
$48.20 unbilled — invoice Acme Corp
+

This month's costs are ready to bill. Sending Acme Corp's invoice now ensures you get paid on time before month end.

+ +
+
+ +
+
Monthly revenue
$210
→ Stable
+
Active users
38
Acme Corp team
+
LLM costs
$29.20
This month
+
+ +
+
+
Site performance
+
+
Product is live and stable
No downtime reported
+
👥
38 active users
Acme Corp team actively using the product
+
⚠️
Invoice not sent
$48.20 has not been billed yet
+
+
+
+
Costs & billing
+
+
+
Unbilled this month
$48.20
+ +
+
LLM usage$29.20
+
Compute$11.60
+
Other$7.40
+
+
+
+
+
+ + + +
+
+
+
+
+
T
+

Taskly

+ Building + Beta Labs +
+

Task management tool for distributed remote teams

+
+
+ +
+
+ +
+
⚙️
+
+
What to do next
+
Complete payment integration — you're almost there
+

Payment is the last major feature before launch. Finishing it this week keeps you on track for the March 30 release date and gets Beta Labs live on time. You've already completed 3 of 5 milestones.

+ +
+
+ +
+
Build progress
60%
+
Current milestone
Payment
Stripe integration
+
Days active
14
Since Mar 9
+
+ +
+
+
Milestone roadmap3 of 5 done · Due Mar 30
+
+
Discover & design
Mar 9
+
Authentication & user accounts
Mar 13
+
Core task features
Mar 19
+
Payment integration
Stripe checkout · In progress
Due Mar 30
+
Launch & handoff to Beta Labs
Mar 31
+
+
+
+ +
+
+
Recent activity
+
+
Checkout page deployed
LLM generated — $1.24
6h ago
+
Auth flow completed
Login, register & reset working
Yesterday
+
Design approved by Beta Labs
Ready to proceed
Mar 12
+
+
+
+
Costs & billing
+
+
+
+
Accrued costs
+
$12.40
+
Billed at next milestone delivery
+
+
+
LLM usage$9.20
+
Compute$3.20
+
Other$0.00
+
+
+
+
+
+ + + +
+
+
+
+
+
N
+

Notion Clone

+ Draft +
+

Personal knowledge base and note-taking tool — idea stage

+
+
+ +
+
+ +
+
💡
+
+
What to do next
+
Define your project before you start building
+

A clear concept saves weeks of rework. Before writing a single line of code, get clear on who this is for, what problem it solves, and what makes it different.

+ +
+
+ +
+
Stage
Idea
Not started yet
+
Created
Mar 21
2 days ago
+
Setup progress
20%
+
+ +
+
+
Getting started1 of 5 done
+
+
Name & description
Added Mar 21
+
Define your target audience
Who is this built for, specifically?
+
List your core features
What makes this different from Notion?
+
Choose your tech stack
+
Set your first milestone
+
+
+
+
Things to think about
+
+
🎯 Be specific about your audience
"People who take notes" is too broad. Try "freelance designers who manage client projects in one place."
+
🔍 Find your unfair advantage
What does Notion do wrong? That's where your positioning lives.
+
📦 Keep your MVP tiny
Launch with one core use case done exceptionally well.
+
+
+
+
+
+ + + +
+
+
+
+

Clients

+

2 active clients · 2 projects

+
+
+ +
+
+ +
+ + +
+
+
+
+
Acme Corp
+
contact@acme.com
+
+ +
+
+
+ Flowmatic + Live +
+
+
MRR
$210
+
Unbilled
$48.20
+
+
+ + +
+
+
+ + +
+
+
+
+
Beta Labs
+
hello@betalabs.io
+
+ +
+
+
+ Taskly + Building +
+
+
Progress
60%
+
Unbilled
$12.40
+
+
+ + +
+
+
+ +
+
+
+ + + +
+
+
+
+

Invoices

+

$60.60 ready to bill across 2 clients

+
+
+ +
+
+ + +
+
💰
+
$60.60 unbilled this month
2 client projects have costs ready to invoice. Send them before month end.
+ +
+ + +
+
Pending invoices
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
Project / ClientPeriodLLMComputeTotalStatus
Flowmatic
Acme Corp
March 2026$29.20$11.60$48.20Unbilled
Taskly
Beta Labs
March 2026$9.20$3.20$12.40Unbilled
+
+ + +
Past invoices
+
+ + + + + + + + + + + +
Project / ClientPeriodTotalStatus
Flowmatic
Acme Corp
February 2026$34.70Paid
+
+ +
+
+ + + +
+
+
+
+

Cost tracker

+

$57.40 spent this month across all projects

+
+
+ + +
+
Total this month
$57.40
↑ +18% vs last month
+
LLM usage
$38.40
67% of total costs
+
Compute
$14.80
26% of total costs
+
Other
$4.20
Email & services
+
+ + +
By project
+
+ + + + + + + + + + + + + + + + + + + + + + +
ProjectLLMComputeOtherTotalBillable to
L
Launchpad
$12.80$4.20$0.40$17.40Personal
F
Flowmatic
$17.00$8.00$3.20$28.20Acme Corp
T
Taskly
$8.60$2.60$0.60$11.80Beta Labs
+
+ + +
Recent charges
+
+ + + + + + + + + +
TimeDescriptionProjectCost
2h agoLLM: Homepage copy generationFlowmatic$0.82
3h agoLLM: Checkout page codeTaskly$1.24
5h agoLLM: Newsletter draftFlowmatic$0.43
6h agoCompute: Build pipeline runTaskly$0.18
YesterdayEmail delivery · 240 recipientsLaunchpad$0.96
+
+ +
+
+ + + +
+
+
+
+

Account settings

+

Manage your profile, plan, and preferences

+
+
+ + +
Profile
+
+
+
+ Full name + Jane Doe +
+
+ Email + jane@example.com +
+
+ Password + +
+
+
+ + +
Plan & billing
+
+
+
+ Current plan + Pro +
+
+ Credits remaining + 448 credits +
+
+ Next billing date + Apr 1, 2026 +
+
+
+ + +
Preferences
+
+
+
+ Appearance + +
+
+
+ +
+
+ + +
+
+
+

Projects

+ +
+
+
+
L
+
+
Launchpad
+
Personal
+
+ Live +
+
+
F
+
+
Flowmatic
+
Acme Corp
+
+ Live +
+
+
T
+
+
Taskly
+
Beta Labs
+
+ Building +
+
+
N
+
+
Notion Clone
+
Personal
+
+ Draft +
+
+
+
+ +
+
+ + + +
+
Edit project
+ +
Colour
+
+
+ + +
+
+ + +
+
Edit client
+ + +
+ + +
+
+ + + + + + + + + + + diff --git a/justine/05_describe.html b/justine/05_describe.html new file mode 100644 index 0000000..29db450 --- /dev/null +++ b/justine/05_describe.html @@ -0,0 +1,1189 @@ + + + + +vibn — Describe + + + + + +
+ + +
+ +
+ + +
+ + +
+
+
+

Your progress is saved.

+

You can come back to this project anytime from your dashboard — everything will be exactly where you left it.

+ + +
+
+ + + + + +
+ +
+
+ + + + + +
+
+
+
Describe
+
+ +
+
◇ Describe
+
⬡ Architect
+
◈ Design
+
✦ Market
+
▲ Build MVP
+
+
Tell me about your idea — I'll turn it into a product plan
+
+
+
+
+
+
+
+
+
+ +
+ + +
V
Let's build your product plan. I'll ask you 6 quick questions — your answers fill your plan in real time. + +IDEA: Describe your product idea in one sentence. What does it do and who is it for?
+
+
+ +
+ +
+ + +
+
+
+ + +
+
+
Your product plan
+
Your plan builds as you answer.
+
+
+ + +
+
+
+
Idea
+
Waiting…
+ What does your product do? +
+ +
+
+ + +
+
+
+
Problem
+
Waiting…
+ What pain does it solve? +
+ +
+
+ + +
+
+
+
Users
+
Waiting…
+ Who is your ideal first customer? +
+ +
+
+ + +
+
+
+
Value
+
Waiting…
+ What will they love most? +
+ +
+
+ + +
+
+
+
Revenue
+
Waiting…
+ How will you charge for it? +
+ +
+
+ + +
+
+
+
Features
+
Waiting…
+ 3 must-have features for v1 +
+ +
+
+ +
+ + + +
+

vibn will now pick the best tech stack for your idea.

+ + + +
Answer all 6 questions to continue
+
+
+
+ + + + +
+
+

Edit this field

+

How would you like to update this?

+
+ + +
+
+ +
+
+
+ + +
+
+

Edit field

+

Update the text below:

+ +
+ + +
+
+
+ + diff --git a/justine/06_architect.html b/justine/06_architect.html new file mode 100644 index 0000000..193ce55 --- /dev/null +++ b/justine/06_architect.html @@ -0,0 +1,589 @@ + + + + +vibn — Architect + + + + + +
+ + +
+ +
+ + +
+ + +
+
+
+
V
+ vibn +
+ +
+
MVP Setup
+
+ + + + + +
+
+ + +
+
+ + +
+ + +
+
+
Your product blueprint
+
We set everything up — review and confirm to continue.
+
+
+ + +
+ + +
+
V
+
+ Here's the technical stack we've set up for your product. These are the best defaults for an idea like yours — review each decision below and change anything that doesn't feel right. +
+
+ + +
+ + +
+
+ How vibn will build it + +
+ + +
+
+
+
Frontend
+
Where will your users mostly be when they use your product — at a desk, or on the go? This shapes every screen we design.
+
+
+ + +
+
+ + +
+
+
+
Backend & Database
+
The invisible part that stores your users' data and makes everything work behind the scenes
+
+
+ + +
+
+ + +
+
+
+
Sign up & Login
+
How people create an account and get back in — fewer steps means more people actually sign up
+
+
+ + +
+
+ + +
+
+
+
Payments
+
How you get paid — Stripe handles the card processing so you never touch sensitive data
+
+
+ + +
+
+ + +
+
+
+
Email
+
Automated messages sent to your users — from welcome emails on day one to newsletters later
+
+
+ + +
+
+ + +
+
+
+
Hosting
+
Where your product lives — on your own servers, so no one else controls your data or your costs
+
+
+ + +
+
+ +
+ + +
+ 💬 +

Not sure about any of these? Don't worry — you can change them anytime before we start building.

+
+ +
+
+ + +
+ + +
+
+
What your users will be able to do
+
10 screens covering the full user journey, ready to design.
+
+
+ + +
+ + +
Pages
+
+ +
+ Public +
+
Discover your product
+
See your pricing
+
Learn about you
+ +
+ Auth +
+
Create an account
+
Sign back in
+
Reset their password
+ +
+ App +
+
Use the dashboard
+
Manage their settings
+ +
+ Payments +
+
Subscribe and pay
+
Manage their plan
+ +
+ + +
Infrastructure
+
+
+
🖥
+
Your own servers
No platform lock-in, ever
+
+
+
🔁
+
Auto-deploy via Coolify
Every push goes live instantly
+
+
+
🔒
+
Code stored in Gitea
Private repo, yours alone
+
+
+ + +
+ ~3–4 weeks to build + · + 💰 No platform fees +
+ +
+ + + + +
+ +
+ + + + + +
+
+
+

Your progress is saved.

+

You can come back to this project anytime from your dashboard — everything will be exactly where you left it.

+ + +
+
+ + + + + + diff --git a/justine/README.md b/justine/README.md new file mode 100644 index 0000000..8d70cee --- /dev/null +++ b/justine/README.md @@ -0,0 +1,44 @@ +# vibn — UX Screen Pack + +Design system: **Ink & parchment** — Lora serif + Inter sans, no colour accent. +All HTML files open directly in any browser — no build step required. + +## Complete screen inventory + +| File | Screen | Interactive? | +|------|--------|-------------| +| `01_homepage.html` | Marketing homepage | Scroll | +| `02_signup.html` | Sign up (3 steps) | ✓ Click through steps, mode selection | +| `03_dashboard.html` | Projects dashboard | ✓ Projects / Clients / Invoices / Costs nav | +| `04_welcome.html` | Welcome phase | Static | +| `05_discover.html` | Discover phase | ✓ Live chat, PRD fills in | +| `06_architect.html` | Architect phase | ✓ Change modal per block | +| `07_design.html` | Design phase | ✓ Live style preview switching | +| `08_market.html` | Market phase | ✓ Voice sliders, Topics, Website style | +| `09_build.html` | Build phase | ✓ Review + animated 12-step pipeline | +| `10_vibe_editor.html` | Vibe editor (post-launch) | ✓ Chat, preview updates, deploy status | +| `vibn-website.jsx` | Marketing site | React component | +| `vibn-dashboard.jsx` | Dashboard + billing | React component | +| `00_design-tokens.css` | Design tokens | Reference | + +## Full user flow +01 Homepage → 02 Sign up → 03 Dashboard → 04 Welcome → +05 Discover → 06 Architect → 07 Design → 08 Market → +09 Build → 10 Vibe editor + +## Design tokens + +| Token | Value | Usage | +|-------|-------|-------| +| `--ink` | `#1a1510` | Primary text, buttons | +| `--ink3` | `#444441` | Secondary text | +| `--mid` | `#5f5e5a` | Body text | +| `--muted` | `#888780` | Labels, captions | +| `--stone` | `#b4b2a9` | Disabled, hints | +| `--cream` | `#f1efe8` | Surface tint, hover | +| `--paper` | `#f7f4ee` | Page background | +| `--white` | `#fdfcfa` | Card backgrounds | +| `--border` | `#e8e2d9` | All borders | + +Heading font: **Lora** (serif) +Body font: **Inter** (sans-serif) diff --git a/justine/favicon_clean.ico b/justine/favicon_clean.ico new file mode 100644 index 0000000..5662842 Binary files /dev/null and b/justine/favicon_clean.ico differ diff --git a/justine/google-auth-popup.html b/justine/google-auth-popup.html new file mode 100644 index 0000000..606ad60 --- /dev/null +++ b/justine/google-auth-popup.html @@ -0,0 +1,119 @@ + + + + +Sign in – Google Accounts + + + + + +
+ + + + +

Sign in

+

to continue to vibn

+ + +
+ + + + +
+ +

To continue, Google will share your name, email address, and profile picture with vibn.

+
+ + +
+
+
Signing you in…
+
+ +
+ + + + + + diff --git a/justine/index.html b/justine/index.html new file mode 100644 index 0000000..482d12b --- /dev/null +++ b/justine/index.html @@ -0,0 +1,41 @@ + + + + + + vibn — UX screen pack (local) + + + + +

vibn — UX screen pack

+

Static HTML prototypes. Suggested flow:

+
    +
  1. 01 — Homepage
  2. +
  3. 02 — Sign up
  4. +
  5. 03 — Dashboard
  6. +
  7. 05 — Describe / discover
  8. +
  9. 06 — Architect
  10. +
+

Google auth popup · Design tokens (CSS)

+

vibn-website.jsx and vibn-dashboard.jsx are React source references — use the HTML screens here, or wire those into a React app separately.

+ + diff --git a/justine/master-ai.code-workspace b/justine/master-ai.code-workspace new file mode 100644 index 0000000..bab1b7f --- /dev/null +++ b/justine/master-ai.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": ".." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/justine/package-lock.json b/justine/package-lock.json new file mode 100644 index 0000000..f2c586c --- /dev/null +++ b/justine/package-lock.json @@ -0,0 +1,1055 @@ +{ + "name": "justine-vibn-ux", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "justine-vibn-ux", + "devDependencies": { + "serve": "^14.2.4" + } + }, + "node_modules/@zeit/schemas": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chalk-template/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk-template/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-port-reachable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", + "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/serve": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.6.tgz", + "integrity": "sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zeit/schemas": "2.36.0", + "ajv": "8.18.0", + "arg": "5.0.2", + "boxen": "7.0.0", + "chalk": "5.0.1", + "chalk-template": "0.4.0", + "clipboardy": "3.0.0", + "compression": "1.8.1", + "is-port-reachable": "4.0.0", + "serve-handler": "6.1.7", + "update-check": "1.5.4" + }, + "bin": { + "serve": "build/main.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/serve-handler": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.5", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-check": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", + "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + } + } +} diff --git a/justine/package.json b/justine/package.json new file mode 100644 index 0000000..724f14b --- /dev/null +++ b/justine/package.json @@ -0,0 +1,11 @@ +{ + "name": "justine-vibn-ux", + "private": true, + "scripts": { + "dev": "serve -l 3040", + "start": "serve -l 3040" + }, + "devDependencies": { + "serve": "^14.2.4" + } +} diff --git a/justine/vibn front end/00_design-tokens.css b/justine/vibn front end/00_design-tokens.css new file mode 100644 index 0000000..c2468f2 --- /dev/null +++ b/justine/vibn front end/00_design-tokens.css @@ -0,0 +1,26 @@ +/* vibn Design Tokens — Ink & Parchment */ +/* Import this in any HTML file or reference these values */ + +@import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;0,700;1,400;1,600&family=Inter:wght@400;500;600&display=swap'); + +:root { + --ink: #1a1510; + --ink2: #2c2c2a; + --ink3: #444441; + --mid: #5f5e5a; + --muted: #888780; + --stone: #b4b2a9; + --parch: #d3d1c7; + --cream: #f1efe8; + --paper: #f7f4ee; + --white: #fdfcfa; + --border: #e8e2d9; + + --serif: 'Lora', Georgia, serif; + --sans: 'Inter', sans-serif; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } +body { font-family: var(--sans); background: var(--paper); color: var(--ink); } +.f { font-family: var(--serif); } +.s { font-family: var(--sans); } diff --git a/justine/vibn front end/01_homepage.html b/justine/vibn front end/01_homepage.html new file mode 100644 index 0000000..f947a1d --- /dev/null +++ b/justine/vibn front end/01_homepage.html @@ -0,0 +1,335 @@ + + + + +vibn — Homepage + + + + + + + + + + + + +
+
+ + +
+
For non-technical founders
+

+ You have the idea.
We handle
everything else. +

+

You describe it. Vibn builds it, launches it, and markets it. From idea to live product in 72 hours — no code, no agencies, no waiting.

+
+ + +
+
+ + +
+
Your idea
+

"I want to build a booking tool for independent personal trainers."

+
+ ↵ Enter +
+
+ + +
+
vibn generated
+
+
+ Pages + Landing, Dashboard, Booking, Payments +
+
+ Stack + Auth, database, payments — handled +
+
+ Revenue + Subscription · $29 / mo +
+
+ Status + ⬤  Ready to build +
+
+
+ +
+
+ +
+ + +
+ +
★★★★★  280 founders launched
+

No credit card required · Free forever plan

+ See how it works → +
+ +
+ + +
+
+
+
+
Sound familiar?
+

The idea is the hard part. Everything else shouldn't be.

+

You know exactly what you want to build and who it's for. But the moment you think about servers, databases, deployment pipelines, SEO — the whole thing stalls.

+

vibn exists to remove all of that. Not abstract it — remove it entirely.

+
+
+
No more "I need to hire a developer first"
vibn is your developer. Start building the moment you have an idea.
+
No more staring at a blank marketing calendar
AI generates and publishes your content every single week.
+
No more "I'll launch when it's ready"
Most founders ship their first version in under 72 hours.
+
+
+
+
+ + +
+
How it works
+

Four phases. One complete product.

+
+
01 — Discover
Define your idea

Six guided questions turn a rough idea into a full product plan — pages, architecture, revenue model. No jargon.

+
02 — Design
Choose your style

Pick a visual style and see your exact site and emails live before a single line of code is written.

+
03 — Build
Your app, live

AI writes every line. Auth, database, payments, all pages — deployed and live. Describe changes in plain English.

+
04 — Grow
Market & automate

AI generates your blog, emails, and social schedule — publishing on autopilot so you can focus on users.

+
+
+ + +
+
+ +
+
+
A live, working product
+

Not a prototype. Real auth, real payments, real database — on your own URL from day one.

+

Runs on your own servers — your data, your infrastructure, no lock-in.

+
+ +
+
+
A full marketing engine
+

Blog posts, onboarding emails, and social content — written and published automatically every week.

+
+ +
+
+
A product that evolves
+

Describe changes in plain English. Vibn handles the code so your product grows as fast as your ideas.

+
+ +
+
+ + +
+
+ +
+ + +
+
+
+

"I had the idea for 2 years. The backend terrified me. vibn shipped it in 4 days and handles all my marketing."

+ — Alex K., founder of Taskly +
+
+ + +
+
+

"I have zero coding experience. Three weeks in, I have 300 paying users. That's entirely because of vibn."

+ — Marcus L., founder of Flowmatic +
+ + +
+
+
+

"The marketing autopilot saved me ten hours a week. My blog runs itself. I just focus on product."

+ — Sara R., founder of Nudge +
+
+ +
+ + +
+
+
+
+
+ +
+
+ + +
+
+
280+
founders launched
+
72h
average time to first version
+
4.9★
average rating
+
faster than hiring a developer
+
+
+ + +
+
+

Your idea deserves to exist.

+

Thousands of ideas never make it past a spreadsheet. Yours doesn't have to be one of them.

+ +
Joins 280+ non-technical founders already live
+
+
+ + + + + + + diff --git a/justine/vibn front end/02_signup.html b/justine/vibn front end/02_signup.html new file mode 100644 index 0000000..7b8f431 --- /dev/null +++ b/justine/vibn front end/02_signup.html @@ -0,0 +1,329 @@ + + + + +vibn — Sign up + + + + + + + +
+
+ + +
+
+
1
+ Account +
+
+
+
2
+ Your experience +
+
+
+
3
+ Ready +
+
+ + +
+

Let's build your first product.

+

Free to start · No credit card needed

+ + + + +
+
+ or continue with email +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+ + +

Joining 280+ founders already building

+ + +

By continuing you agree to our Terms and Privacy Policy

+
+
+ + + + + + + +
+
+ + + + diff --git a/justine/vibn front end/03_dashboard.html b/justine/vibn front end/03_dashboard.html new file mode 100644 index 0000000..fc889e3 --- /dev/null +++ b/justine/vibn front end/03_dashboard.html @@ -0,0 +1,2012 @@ + + + + + +vibn — Dashboard + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + +
+
+

Good morning, Jane.

+

Here's what's happening across your products today.

+
+
+ + +
+
+ + + + + +
Portfolio snapshot
+
+
+
4
+
Active projects
+
+
+
2
+
Live products
+
+
+
1
+
Building now
+
+
+
$48.20
+
Unbilled revenue
+
+
+ + +
+
Overall performance
+ Aggregated across all projects +
+
+ +
+
Monthly Revenue
+
$1,050
+
↑ +$60 from last month
+
Launchpad $840 · Flowmatic $210
+
+ + + + + + +
+
+ +
+
Active Users
+
180
+
↑ +12 this week
+
Across 2 live products
+
+ + + + + + +
+
+ +
+
Build Progress
+
60%
+
↑ Taskly on track
+
Payment milestone due Mar 30
+
+
+
+
+
+ 3 of 5 milestones done + Due Mar 30 +
+
+
+ +
+ + +
Next milestones
+
+
+ + +
+
+
+
+
Payment integration
+
Taskly · Stripe checkout
+
+
+ In progress +
+
Mar 30
+
7 days left
+
+
+ + +
+
+
+
+
Launch & handoff
+
Taskly · Beta Labs delivery
+
+
+ Upcoming +
+
Mar 31
+
8 days left
+
+
+ + +
+
+
+
+
Reach 200 active users
+
Launchpad · Currently 142
+
+
+ On track +
+
Apr 15
+
23 days left
+
+
+ + +
+
+
+
+
Complete project setup
+
Notion Clone · Define & scope
+
+
+ Not started +
+
No date
+
+
+ +
+
+ + +
Helpful resources
+
+ +
+
📊
+
SaaS Metrics 101
+
Learn the key numbers every SaaS founder should track — MRR, churn, LTV, CAC, and what they actually mean for your business.
+ +
+ +
+
🚀
+
From idea to MVP
+
A step-by-step guide to turning a rough concept into a working product people will pay for — without overbuilding or wasting time.
+ +
+ +
+
💰
+
Pricing your SaaS
+
How to pick a pricing model, set your first price, and increase it over time — with real examples from indie makers who got it right.
+ +
+ +
+ +
+
+ + + +
+
+
+
+
+
L
+

Launchpad

+ Live + + + launchpad.vibn.app + +
+

AI-powered launch page builder for indie makers

+
+
+ + + +
+
+ +
+
📈
+
+
What to do next
+
Focus on conversion this week
+

You're pulling 2,400 visitors per month but only 6% convert to paying users. Improving your pricing page or onboarding flow could unlock significant growth — even a 2% lift adds $168/mo in MRR.

+ +
+
+ +
+
Monthly revenue
$840
↑ +$60 this month
+
Active users
142
3 new this week
+
Visitors / month
2,400
→ Stable
+
Conversion rate
8%
↑ from 6% last month
+
+ +
+
+
Site performance
+
+
Traffic is stable
2,400 visitors/mo — no drop detected
+
📈
MRR growing +7.7%
$780 → $840 vs last month
+
👥
3 new paying users
This week · 0 churn this month
+
📈
Conversion improving
8% vs 6% last month
+
+
+
+
Recent activity
+
+
Blog post published
"How to launch faster with AI"
2h ago
+
New signup
marcus@email.com
5h ago
+
Newsletter #12 scheduled
Sends tomorrow at 9am
Yesterday
+
+
+
+
Revenue summary
+
+
Monthly recurring revenue$840
+
Unbilled$0 — all clear
+
Billing typePersonal product
+
+
+
+
Recommendations
+
+
💡 Run an A/B test on pricing
Industry conversion for your category is 8–12%. At 6%, a better pricing page could add $100–200/mo.
+
📦 Consider annual pricing
An annual plan typically improves LTV by 30% and gives you better cash flow predictability.
+
+
+
+
+
+ + + +
+
+
+
+
+
F
+

Flowmatic

+ Live + Acme Corp + + + flowmatic.app + +
+

Workflow automation tool built for Acme Corp

+
+
+ + +
+
+ +
+
💰
+
+
Ready to bill
+
$48.20 unbilled — invoice Acme Corp
+

This month's costs are ready to bill. Sending Acme Corp's invoice now ensures you get paid on time before month end.

+ +
+
+ +
+
Monthly revenue
$210
→ Stable
+
Active users
38
Acme Corp team
+
Visitors / month
620
→ Stable
+
Conversion rate
6.1%
→ Stable
+
+ +
+
+
Site performance
+
+
Product is live and stable
No downtime reported
+
👥
38 active users
Acme Corp team actively using the product
+
⚠️
Invoice not sent
$48.20 has not been billed yet
+
+
+
+
Costs & billing
+
+
+
Unbilled this month
$48.20
+ +
+
LLM usage$29.20
+
Compute$11.60
+
Other$7.40
+
+
+
+
+
+ + + +
+
+
+
+
+
T
+

Taskly

+ Building + Beta Labs +
+

Task management tool for distributed remote teams

+
+
+ +
+
+ +
+
⚙️
+
+
What to do next
+
Complete payment integration — you're almost there
+

Payment is the last major feature before launch. Finishing it this week keeps you on track for the March 30 release date and gets Beta Labs live on time. You've already completed 3 of 5 milestones.

+ +
+
+ +
+
Build progress
60%
+
Current milestone
Payment
Stripe integration
+
Days active
14
Since Mar 9
+
+ +
+
+
Milestone roadmap3 of 5 done · Due Mar 30
+
+
Discover & design
Mar 9
+
Authentication & user accounts
Mar 13
+
Core task features
Mar 19
+
Payment integration
Stripe checkout · In progress
Due Mar 30
+
Launch & handoff to Beta Labs
Mar 31
+
+
+
+ +
+
+
Recent activity
+
+
Checkout page deployed
LLM generated — $1.24
6h ago
+
Auth flow completed
Login, register & reset working
Yesterday
+
Design approved by Beta Labs
Ready to proceed
Mar 12
+
+
+
+
Costs & billing
+
+
+
+
Accrued costs
+
$12.40
+
Billed at next milestone delivery
+
+
+
LLM usage$9.20
+
Compute$3.20
+
Other$0.00
+
+
+
+
+
+ + + +
+
+
+
+
+
N
+

Notion Clone

+ Draft +
+

Personal knowledge base and note-taking tool — idea stage

+
+
+ +
+
+ +
+
💡
+
+
What to do next
+
Define your project before you start building
+

A clear concept saves weeks of rework. Before writing a single line of code, get clear on who this is for, what problem it solves, and what makes it different.

+ +
+
+ +
+
Stage
Idea
Not started yet
+
Created
Mar 21
2 days ago
+
Setup progress
20%
+
+ +
+
+
Getting started1 of 5 done
+
+
Name & description
Added Mar 21
+
Define your target audience
Who is this built for, specifically?
+
List your core features
What makes this different from Notion?
+
Choose your tech stack
+
Set your first milestone
+
+
+
+
Things to think about
+
+
🎯 Be specific about your audience
"People who take notes" is too broad. Try "freelance designers who manage client projects in one place."
+
🔍 Find your unfair advantage
What does Notion do wrong? That's where your positioning lives.
+
📦 Keep your MVP tiny
Launch with one core use case done exceptionally well.
+
+
+
+
+
+ + + +
+
+
+
+

Clients

+

2 active clients · 2 projects

+
+
+ +
+
+ +
+ + +
+
+
+
+
Acme Corp
+
contact@acme.com
+
+ +
+
+
+ Flowmatic + Live +
+
+
MRR
$210
+
Unbilled
$48.20
+
+
+ + +
+
+
+ + +
+
+
+
+
Beta Labs
+
hello@betalabs.io
+
+ +
+
+
+ Taskly + Building +
+
+
Progress
60%
+
Unbilled
$0
+
+
+ +
+
+
+ +
+
+
+ + + +
+
+
+
+

Invoices

+

$48.20 ready to bill across 1 client

+
+
+ +
+
+ + +
+
💰
+
$48.20 unbilled this month
1 client project has costs ready to invoice. Send it before month end.
+ +
+ + +
+
Pending invoices
+
+
+ + + + + + + + + + + + + +
Project / ClientPeriodLLMComputeTotalStatus
Flowmatic
Acme Corp
March 2026$29.20$11.60$48.20Unbilled
+
+ + +
Past invoices
+
+ + + + + + + + + + + +
Project / ClientPeriodTotalStatus
Flowmatic
Acme Corp
February 2026$34.70Paid
+
+ +
+
+ + + +
+
+
+
+

Cost tracker

+

$57.40 spent this month across all projects

+
+
+ + +
+
Total this month
$57.40
↑ +18% vs last month
+
LLM usage
$38.40
67% of total costs
+
Compute
$14.80
26% of total costs
+
Other
$4.20
Email & services
+
+ + +
By project
+
+ + + + + + + + + + + + + + + + + + + + + + +
ProjectLLMComputeOtherTotalBillable to
L
Launchpad
$12.80$4.20$0.40$17.40Personal
F
Flowmatic
$17.00$8.00$3.20$28.20Acme Corp
T
Taskly
$8.60$2.60$0.60$11.80Beta Labs
+
+ + +
Recent charges
+
+ + + + + + + + + +
TimeDescriptionProjectCost
2h agoLLM: Homepage copy generationFlowmatic$0.82
3h agoLLM: Checkout page codeTaskly$1.24
5h agoLLM: Newsletter draftFlowmatic$0.43
6h agoCompute: Build pipeline runTaskly$0.18
YesterdayEmail delivery · 240 recipientsLaunchpad$0.96
+
+ +
+
+ + + +
+
+
+
+

Account settings

+

Manage your profile, plan, and preferences

+
+
+ +
+
+ + + + + +
Profile
+
+
+
+ Full name + Jane Doe + +
+
+ Email + jane@example.com + +
+
+ Password + +
+
+
+ + +
Plan & billing
+
+
+
+ Current plan +
+ Pro + +
+
+
+ Credits remaining + 448 credits +
+
+ Next billing date + Apr 1, 2026 +
+
+
+ + +
Preferences
+
+
+
+ Appearance + +
+
+
+ +
+
+ + +
+
+
+

Projects

+ +
+
+
+
L
+
+
Launchpad
+
Personal
+
+ Live +
+
+
F
+
+
Flowmatic
+
Acme Corp
+
+ Live +
+
+
T
+
+
Taskly
+
Beta Labs
+
+ Building +
+
+
N
+
+
Notion Clone
+
Personal
+
+ Draft +
+
+
+
+ +
+
+ + + +
+
Edit project
+ +
Colour
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/justine/vibn front end/05_describe.html b/justine/vibn front end/05_describe.html new file mode 100644 index 0000000..8086bc3 --- /dev/null +++ b/justine/vibn front end/05_describe.html @@ -0,0 +1,1221 @@ + + + + + +vibn — Describe + + + + + +
+ + +
+ +
+ + +
+ + +
+
+
+

Your progress is saved.

+

You can come back to this project anytime from your dashboard — everything will be exactly where you left it.

+ + +
+
+ + + + + +
+ +
+
+ + + + + +
+
+
+
Describe
+
+ +
+
◇ Describe
+
⬡ Architect
+
◈ Design
+
✦ Website
+
▲ Build MVP
+
+
Tell me about your idea — I'll turn it into a product plan
+
+
+
+
+
+
+
+
+
+ +
+ + +
V
Let's build your product plan. I'll ask you 6 quick questions — your answers fill your plan in real time. + +IDEA: Describe your product idea in one sentence. What does it do and who is it for?
+
+
+ +
+ +
+ + +
+
+
+ + +
+
+
Your product plan
+
Your plan builds as you answer.
+
+
+ + +
+
+
+
Idea
+
Waiting…
+ What does your product do? +
+ +
+
+ + +
+
+
+
Problem
+
Waiting…
+ What pain does it solve? +
+ +
+
+ + +
+
+
+
Users
+
Waiting…
+ Who is your ideal first customer? +
+ +
+
+ + +
+
+
+
Value
+
Waiting…
+ What will they love most? +
+ +
+
+ + +
+
+
+
Revenue
+
Waiting…
+ How will you charge for it? +
+ +
+
+ + +
+
+
+
Features
+
Waiting…
+ 3 must-have features for v1 +
+ +
+
+ +
+ + + +
+

vibn will now pick the best tech stack for your idea.

+ + + +
Answer all 6 questions to continue
+
+
+
+ + + + +
+
+

Edit this field

+

How would you like to update this?

+
+ + +
+
+ +
+
+
+ + +
+
+

Edit field

+

Update the text below:

+ +
+ + +
+
+
+ + diff --git a/justine/vibn front end/06_architect.html b/justine/vibn front end/06_architect.html new file mode 100644 index 0000000..49f4a36 --- /dev/null +++ b/justine/vibn front end/06_architect.html @@ -0,0 +1,638 @@ + + + + + +vibn — Architect + + + + + +
+ + +
+ +
+ + +
+ + + + + +
+ + +
+
+
Your product blueprint
+
We’ve translated your idea into a complete system — how it’s built, how it runs, and how users interact with it. Review and confirm to continue
+
+
+ + +
+ + +
+
V
+
+ Here's the technical stack we've set up for your product. These are the best defaults for an idea like yours — review each decision below and change anything that doesn't feel right. +
+
+ + +
+ + +
+
+ How vibn will build it + +
+ + +
+
+
+
Frontend
+
Where will your users mostly be when they use your product — at a desk, or on the go? This shapes every screen we design.
+
+
+ + +
+
+ + +
+
+
+
Backend & Database
+
The invisible part that stores your users' data and makes everything work behind the scenes
+
+
+ + +
+
+ + +
+
+
+
Sign up & Login
+
How people create an account and get back in — fewer steps means more people actually sign up
+
+
+ + +
+
+ + +
+
$
+
+
Payments
+
How you get paid — Stripe handles the card processing so you never touch sensitive data
+
+
+ + +
+
+ + +
+
+
+
Email
+
Automated messages sent to your users — from welcome emails on day one to newsletters later
+
+
+ + +
+
+ + +
+
+
+
Hosting
+
Where your product lives — on your own servers, so no one else controls your data or your costs
+
+
+ + +
+
+ +
+ + +
+ 💬 +

Not sure about any of these? Don't worry — you can change them anytime before we start building.

+
+ +
+
+ + +
+ + +
+
+
What your users will be able to do
+
10 screens covering the full user journey, ready to design.
+
+
+ + +
+ + +
Pages
+
+ +
+ Public +
+
Discover your product
+
See your pricing
+
Learn about you
+ +
+ Auth +
+
Create an account
+
Sign back in
+
Reset their password
+ +
+ App +
+
Use the dashboard
+
Manage their settings
+ +
+ Payments +
+
Subscribe and pay
+
Manage their plan
+ +
+ + +
Infrastructure
+
+
+
🖥
+
Your own servers
No platform lock-in, ever
+
+
+
🔁
+
Auto-deploy via Coolify
Every push goes live instantly
+
+
+
🔒
+
Code stored in Gitea
Private repo, yours alone
+
+
+ + +
+ ~3–4 weeks to build + · + 💰 No platform fees +
+ +
+ + + + +
+ +
+ + + + + +
+
+
+

Your progress is saved.

+

You can come back to this project anytime from your dashboard — everything will be exactly where you left it.

+ + +
+
+ + + + + + diff --git a/justine/vibn front end/07_design.html b/justine/vibn front end/07_design.html new file mode 100644 index 0000000..32002a4 --- /dev/null +++ b/justine/vibn front end/07_design.html @@ -0,0 +1,1361 @@ + + + + + +vibn — Design + + + + +
+ + + + + +
+ + +
+
+
Your product's look & feel
+
Pick a feel, a color, and a structure — we'll generate a live theme. You can refine everything after launch.
+
+
+ + +
+ + +
+ + +
+ + +
+
Feel
+ + + +
+ +
+ + +
+
Accent color
+
+ + + + Indigo +
+
+ +
+ + +
+
Layout structure
+
+ + + +
+
+ + + + + +
+
+ + +
+
More colors
+
+ + + +
+
+ + +
+
Visual intensity
+
+ + + +
+
+ + +
+
Content density
+
+ + + +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ + +
+ + +
+
+ + Live preview +
+
+ + +
+
+ + +
+
+
+
app.yourproduct.com
+
+
+
+
+
+ +
+ +
+
+ + +
+ + +
+
+
Your app's design
+
How your product will look and feel to users on day one.
+
+
+ + +
+ + +
First impression
+
+ + +
Visual identity
+
+ + +
How users will experience it
+
+ + +
Design detail
+
+ +
+ + + + +
+ +
+ +
+
+
+

Your progress is saved.

+

You can come back to this project anytime from your dashboard — everything will be exactly where you left it.

+ + +
+
+ + + + diff --git a/justine/vibn front end/08_website.html b/justine/vibn front end/08_website.html new file mode 100644 index 0000000..824363a --- /dev/null +++ b/justine/vibn front end/08_website.html @@ -0,0 +1,805 @@ + + + + + +vibn — Website + + + + +
+ + + + + + + +
+
+
+
Website
+
This is what people see before signing up. Set your voice, topics, and website style
+
+
+ +
+ + +
+ + +
+
Voice
+
+
Tone
+
+ + + +
+
+
+
Style
+
+ + + +
+
+
+
Personality
+
+ + + +
+
+
+ +
+ + +
+
Topics
+
+ + + + + +
+
+ +
+ + +
+
Website style
+
+ + + + +
+
+ + +
+
+ +
+ +
+ + +
+ + +
+
+ + Live preview +
+
+ + +
+
+ + +
+
+
+
yourproduct.com
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+ +
+
+ + +
+
+
+
Your brand at a glance
+
A summary of how your brand will look and sound to the world.
+
+
+
+ + +
Your brand voice
+
+ + +
Website experience
+
+ + +
How users will perceive it
+
+ + +
Optimized for
+
+ + + + + + + + +
+
+

All set — let's build it.

+ +
+
+
+ + + diff --git a/justine/vibn front end/09_build.html b/justine/vibn front end/09_build.html new file mode 100644 index 0000000..5052ac2 --- /dev/null +++ b/justine/vibn front end/09_build.html @@ -0,0 +1,412 @@ + + + + + +vibn — Build + + + + +
+ + + + + +
+ + +
+
Ready to build
+

Review everything below. Once you hit Build, AI codes your full product and deploys it.

+
+
What's being built
+
+
Sign up & login
Email + social login
+
$
Payments
Subscription billing
+
Email
Transactional + marketing
+
Product style
Clean & focused
+
Website style
Startup energy
+
Campaign topics
3 topics ready
+
+
+
+
Pages14 pages total
+
+
Public
Landing page
Pricing
About
Blog
+
Auth
Sign up
Log in
Forgot password
+
App
Dashboard
Onboarding
Settings
+
Payments
Checkout
Success
Manage subscription
+
+
+
+
Your design
+
+
+
+
Feel
Friendly
+
+
+
+
Accent
Indigo
+
+
+
+
Layout
Clean
+
+
+
+
+
Your website
+
+
+
+
Voice
Friendly · Balanced · Warm
+
+
+
+
Website style
Editorial
+
+
+
+
Topics
The problem · Who it's for · Why now
+
+
+
+ +
+ +
+
+
You're ready to build your product
+

Your app will be generated, your backend configured, and everything deployed to your infrastructure — fully automated, no code needed.

+
+ +
+
What happens next
+
+
+
Generate UI & all pages
+ ~30s +
+
+
+
Set up database & backend
+ ~45s +
+
+
+
Connect auth, payments & email
+ ~30s +
+
+
+
Deploy your app live
+ ~20s +
+
+

Takes ~2–4 minutes · All steps run in parallel

+ + +

No code needed  ·  You can edit everything after

+
+ + +
+ +
+
+ + + + +
+
+ + + diff --git a/justine/vibn front end/README.md b/justine/vibn front end/README.md new file mode 100644 index 0000000..8d70cee --- /dev/null +++ b/justine/vibn front end/README.md @@ -0,0 +1,44 @@ +# vibn — UX Screen Pack + +Design system: **Ink & parchment** — Lora serif + Inter sans, no colour accent. +All HTML files open directly in any browser — no build step required. + +## Complete screen inventory + +| File | Screen | Interactive? | +|------|--------|-------------| +| `01_homepage.html` | Marketing homepage | Scroll | +| `02_signup.html` | Sign up (3 steps) | ✓ Click through steps, mode selection | +| `03_dashboard.html` | Projects dashboard | ✓ Projects / Clients / Invoices / Costs nav | +| `04_welcome.html` | Welcome phase | Static | +| `05_discover.html` | Discover phase | ✓ Live chat, PRD fills in | +| `06_architect.html` | Architect phase | ✓ Change modal per block | +| `07_design.html` | Design phase | ✓ Live style preview switching | +| `08_market.html` | Market phase | ✓ Voice sliders, Topics, Website style | +| `09_build.html` | Build phase | ✓ Review + animated 12-step pipeline | +| `10_vibe_editor.html` | Vibe editor (post-launch) | ✓ Chat, preview updates, deploy status | +| `vibn-website.jsx` | Marketing site | React component | +| `vibn-dashboard.jsx` | Dashboard + billing | React component | +| `00_design-tokens.css` | Design tokens | Reference | + +## Full user flow +01 Homepage → 02 Sign up → 03 Dashboard → 04 Welcome → +05 Discover → 06 Architect → 07 Design → 08 Market → +09 Build → 10 Vibe editor + +## Design tokens + +| Token | Value | Usage | +|-------|-------|-------| +| `--ink` | `#1a1510` | Primary text, buttons | +| `--ink3` | `#444441` | Secondary text | +| `--mid` | `#5f5e5a` | Body text | +| `--muted` | `#888780` | Labels, captions | +| `--stone` | `#b4b2a9` | Disabled, hints | +| `--cream` | `#f1efe8` | Surface tint, hover | +| `--paper` | `#f7f4ee` | Page background | +| `--white` | `#fdfcfa` | Card backgrounds | +| `--border` | `#e8e2d9` | All borders | + +Heading font: **Lora** (serif) +Body font: **Inter** (sans-serif) diff --git a/justine/vibn front end/favicon_clean.ico b/justine/vibn front end/favicon_clean.ico new file mode 100644 index 0000000..5662842 Binary files /dev/null and b/justine/vibn front end/favicon_clean.ico differ diff --git a/justine/vibn front end/google-auth-popup.html b/justine/vibn front end/google-auth-popup.html new file mode 100644 index 0000000..606ad60 --- /dev/null +++ b/justine/vibn front end/google-auth-popup.html @@ -0,0 +1,119 @@ + + + + +Sign in – Google Accounts + + + + + +
+ + + + +

Sign in

+

to continue to vibn

+ + +
+ + + + +
+ +

To continue, Google will share your name, email address, and profile picture with vibn.

+
+ + +
+
+
Signing you in…
+
+ +
+ + + + + + diff --git a/justine/vibn front end/vibn-dashboard.jsx b/justine/vibn front end/vibn-dashboard.jsx new file mode 100644 index 0000000..50df402 --- /dev/null +++ b/justine/vibn front end/vibn-dashboard.jsx @@ -0,0 +1,484 @@ +// vibn — Projects Dashboard +// Restyled from original (DM Sans + purple/colour accents) → Ink & parchment +// Design: Lora serif + Inter sans, #1a1510 ink, #f7f4ee paper, no colour accent +// Usage: default export, no required props + +import { useState } from "react"; + +const T = { + ink: "#1a1510", + ink2: "#2c2c2a", + ink3: "#444441", + mid: "#5f5e5a", + muted: "#888780", + stone: "#b4b2a9", + parch: "#d3d1c7", + cream: "#f1efe8", + paper: "#f7f4ee", + white: "#fdfcfa", + border: "#e8e2d9", + border2:"#d3d1c7", +}; + +const F = { serif: "'Lora', Georgia, serif", sans: "'Inter', sans-serif" }; + +// ─── Shared primitives ───────────────────────────────────────────────────────── + +function StatusPill({ label, variant = "default" }) { + const styles = { + live: { bg: T.cream, text: T.ink3, border: T.border }, + building: { bg: T.cream, text: T.ink3, border: T.border }, + default: { bg: T.paper, text: T.muted, border: T.border }, + invoiced: { bg: T.ink, text: T.paper, border: T.ink }, + unbilled: { bg: T.cream, text: T.ink3, border: T.border }, + scheduled: { bg: T.parch, text: T.ink2, border: T.border2 }, + }; + const s = styles[variant] || styles.default; + return ( + {label} + ); +} + +function InkBtn({ children, onClick, small, outline }) { + return ( + + ); +} + +// ─── Nav ─────────────────────────────────────────────────────────────────────── + +function Nav({ screen, setScreen }) { + return ( + + ); +} + +// ─── Data ────────────────────────────────────────────────────────────────────── + +const PROJECTS = [ + { + id: "launchpad", label: "Launchpad", initial: "L", + type: "own", status: "live", url: "launchpad.vibn.app", + stats: { visitors: "2.4k", signups: 183, mrr: "$840" }, + }, + { + id: "flowmatic", label: "Flowmatic", initial: "F", + type: "client", status: "live", url: "flowmatic.app", + client: "Acme Corp", + stats: { visitors: "890", signups: 54, mrr: "$210" }, + costs: { total: 48.20, llm: 29.20, compute: 11.60, other: 7.40, billed: false }, + }, + { + id: "taskly", label: "Taskly", initial: "T", + type: "client", status: "building", url: null, + client: "Beta Labs", buildProgress: 60, + costs: { total: 12.40, llm: 9.20, compute: 3.20, other: 0, billed: false }, + }, +]; + +const ACTIVITY = [ + { text: "Launchpad — Blog post published:", detail: '"How to launch faster with AI"', time: "2h ago" }, + { text: "Flowmatic — New signup:", detail: "marcus@email.com", time: "4h ago" }, + { text: "Taskly — Checkout page built and deployed", detail: "", time: "6h ago" }, + { text: "Launchpad — Newsletter #12", detail: "scheduled", time: "Yesterday" }, +]; + +const BILLING_ROWS = [ + { label: "Flowmatic", initial: "F", client: "Acme Corp", llm: 29.20, compute: 11.60, other: 7.40, total: 48.20, billed: false }, + { label: "Taskly", initial: "T", client: "Beta Labs", llm: 9.20, compute: 3.20, other: 0, total: 12.40, billed: false }, + { label: "Flowmatic", initial: "F", client: "Acme · Feb", llm: 22.10, compute: 8.40, other: 4.20, total: 34.70, billed: true }, +]; + +const COST_LOG = [ + { time: "2h ago", desc: "LLM: Homepage copy generation", project: "Flowmatic", cost: 0.82 }, + { time: "3h ago", desc: "LLM: Checkout page code", project: "Taskly", cost: 1.24 }, + { time: "5h ago", desc: "LLM: Weekly newsletter draft", project: "Flowmatic", cost: 0.43 }, + { time: "6h ago", desc: "Compute: Build pipeline run", project: "Taskly", cost: 0.18 }, + { time: "8h ago", desc: "LLM: Discover phase Q&A", project: "Flowmatic", cost: 0.31 }, + { time: "Yesterday", desc: "Email delivery · 240 recipients", project: "Flowmatic", cost: 0.96 }, +]; + +// ─── Project card ────────────────────────────────────────────────────────────── + +function ProjectCard({ project }) { + const isClient = project.type === "client"; + const isBuilding = project.status === "building"; + + return ( +
+ + {/* Header preview */} + {isBuilding ? ( +
+
+
+ Build phase · {project.buildProgress}% complete +
+
+
+
+
+ {isClient && ( +
+ Client +
+ )} +
+ ) : ( +
+
+
+
+
+
+ {isClient ? "Client" : "My product"} +
+
+ )} + +
+ {/* Identity row */} +
+
+
+ {project.initial} +
+
+
{project.label}
+
+ {isClient ? `${project.client} · ` : ""} + {project.url || "Setting up pages…"} +
+
+
+ +
+ + {/* Cost strip — client + building */} + {isClient && project.costs && isBuilding && ( +
+
+
Costs so far
+
${project.costs.total.toFixed(2)}
+
+ +
+ )} + + {/* Cost strip — client + live */} + {isClient && project.costs && !isBuilding && ( +
+
+
Costs this month
+
${project.costs.total.toFixed(2)}
+
+
+ LLM ${project.costs.llm.toFixed(2)}
+ Compute ${project.costs.compute.toFixed(2)} +
+ {!project.costs.billed && ( + + )} +
+ )} + + {/* Stats */} + {!isBuilding && project.stats && ( +
+ {[["visitors", project.stats.visitors], ["signups", project.stats.signups], ["MRR", project.stats.mrr]].map(([k, v]) => ( +
+
{v}
+
{k}
+
+ ))} +
+ )} + + {/* Actions */} + {isBuilding ? ( + + ) : ( +
+ {[["⬡", "Build"], ["◈", "Grow"]].map(([icon, label]) => ( +
+ {icon} + {label} +
+ ))} +
+ ↗ +
+
+ )} +
+
+ ); +} + +// ─── Projects screen ─────────────────────────────────────────────────────────── + +function ProjectsScreen({ setScreen }) { + const totalUnbilled = PROJECTS + .filter(p => p.type === "client" && p.costs?.billed === false) + .reduce((s, p) => s + p.costs.total, 0); + + return ( +
+
+
+

Your projects

+

3 active · 1 building

+
+
+ {totalUnbilled > 0 && ( + + )} + + New project +
+
+ +
+ {PROJECTS.map(p => )} + + {/* New project CTA card */} +
e.currentTarget.style.background = T.cream} + onMouseLeave={e => e.currentTarget.style.background = "transparent"} + > +
+
+
+
New project
+
For yourself or a client
+
+
+
+ + {/* Activity feed */} +
+
Recent activity
+ {ACTIVITY.map((a, i) => ( +
+
+
+ {a.text}{" "}{a.detail && {a.detail}} +
+ {a.time} +
+ ))} +
+
+ ); +} + +// ─── Billing screen ──────────────────────────────────────────────────────────── + +function BillingScreen() { + const [tab, setTab] = useState("billing"); + const unbilled = BILLING_ROWS.filter(r => !r.billed).reduce((s, r) => s + r.total, 0); + + return ( +
+ + {/* Sub-tabs */} +
+ {[["billing", "Client billing"], ["costs", "Cost tracker"]].map(([id, label]) => ( + + ))} +
+ + {tab === "billing" && <> +
+
+

Client billing

+

All costs tracked and ready to invoice

+
+ Generate invoice +
+ +
+ {[ + { label: "Total unbilled", value: `$${unbilled.toFixed(2)}` }, + { label: "LLM costs", value: "$38.40" }, + { label: "Compute", value: "$14.80" }, + { label: "Other", value: "$7.40" }, + ].map(c => ( +
+
{c.label}
+
{c.value}
+
+ ))} +
+ +
+
+ Breakdown by client + +
+
+ {["Project / Client", "LLM", "Compute", "Other", "Total", "Status"].map(h => ( +
{h}
+ ))} +
+ {BILLING_ROWS.map((r, i) => ( +
+
+
{r.initial}
+
+
{r.label}
+
{r.client}
+
+
+
${r.llm.toFixed(2)}
+
${r.compute.toFixed(2)}
+
${r.other.toFixed(2)}
+
${r.total.toFixed(2)}
+
+ + {!r.billed && ( + + )} +
+
+ ))} +
+ } + + {tab === "costs" && <> +
+

Cost tracker

+

Every dollar spent, broken down by type and project

+
+ +
+
+
LLM usage
+ {[ + { label: "Code generation", amount: 21.40, pct: 56 }, + { label: "Content & marketing", amount: 10.20, pct: 27 }, + { label: "Chat assist", amount: 6.80, pct: 18 }, + ].map(r => ( +
+
+ {r.label} + ${r.amount.toFixed(2)} +
+
+
+
+
+ ))} +
+ Total LLM + $38.40 +
+
+ +
+
Infrastructure
+ {[ + { label: "Hosting & compute", amount: 11.60 }, + { label: "Database", amount: 3.20 }, + { label: "Email delivery", amount: 4.20 }, + { label: "Domain & SSL", amount: 3.20 }, + ].map(r => ( +
+ {r.label} + ${r.amount.toFixed(2)} +
+ ))} +
+ Total infra + $22.20 +
+
+
+ +
+
+ Recent charges +
+
+ {["Time", "Description", "Project", "Cost"].map(h => ( +
{h}
+ ))} +
+ {COST_LOG.map((row, i) => ( +
+
{row.time}
+
{row.desc}
+
{row.project}
+
${row.cost.toFixed(2)}
+
+ ))} +
+ } +
+ ); +} + +// ─── Root ────────────────────────────────────────────────────────────────────── + +export default function Dashboard() { + const [screen, setScreen] = useState("projects"); + + return ( +
+ +
+ ); +} diff --git a/justine/vibn front end/vibn-website.jsx b/justine/vibn front end/vibn-website.jsx new file mode 100644 index 0000000..9e5edc9 --- /dev/null +++ b/justine/vibn front end/vibn-website.jsx @@ -0,0 +1,288 @@ +// vibn — Marketing Website +// Design: Ink & parchment — Lora serif + Inter sans, no colour accent +// Usage: + +import { useState } from "react"; + +const T = { + ink: "#1a1510", + ink2: "#2c2c2a", + ink3: "#444441", + mid: "#5f5e5a", + muted: "#888780", + stone: "#b4b2a9", + parch: "#d3d1c7", + cream: "#f1efe8", + paper: "#f7f4ee", + white: "#fdfcfa", + border: "#e8e2d9", +}; + +const F = { serif: "'Lora', Georgia, serif", sans: "'Inter', sans-serif" }; + +// ─── Primitives ──────────────────────────────────────────────────────────────── + +function Eyebrow({ children }) { + return ( +
+ {children} +
+ ); +} + +function PrimaryBtn({ children, onClick, large }) { + return ( + + ); +} + +// ─── Nav ─────────────────────────────────────────────────────────────────────── + +function Nav({ onGetStarted, onLogin }) { + return ( + + ); +} + +// ─── Hero ────────────────────────────────────────────────────────────────────── + +function Hero({ onCta }) { + return ( +
+ For non-technical founders +

+ You have the idea.
+ We handle
+ everything else. +

+

+ No backend. No DevOps. No marketing agency. Describe your idea and vibn + builds, deploys, and promotes it — automatically. +

+
+ Start free — no code needed + ★★★★★  280 founders launched +
+

No credit card required · Free forever plan

+
+ ); +} + +// ─── Quote band ──────────────────────────────────────────────────────────────── + +const QUOTES = [ + { q: "I had the idea for 2 years. The backend terrified me. vibn shipped it in 4 days and handles all my marketing.", by: "Alex K.", role: "Founder, Taskly" }, + { q: "I have zero coding experience. Three weeks in I have 300 paying users. That's entirely because of vibn.", by: "Marcus L.", role: "Founder, Flowmatic" }, + { q: "The marketing autopilot alone saved me ten hours a week. My blog runs itself. I just focus on my product.", by: "Sara R.", role: "Founder, Nudge" }, +]; + +function QuoteBand() { + return ( +
+
+ {QUOTES.map((q, i) => ( +
+
+
+

"{q.q}"

+ — {q.by}, {q.role} +
+
+ ))} +
+
+ ); +} + +// ─── How it works ────────────────────────────────────────────────────────────── + +const PHASES = [ + { n: "01", id: "Discover", title: "Define your idea", body: "Six guided questions turn a rough idea into a full product plan — pages, architecture, revenue model. No jargon." }, + { n: "02", id: "Design", title: "Choose your style", body: "Pick a visual style and see your exact site and emails live before a single line of code is written." }, + { n: "03", id: "Build", title: "Your app, live", body: "AI writes every line. Auth, database, payments, all pages — deployed and live. Describe changes in plain English." }, + { n: "04", id: "Grow", title: "Market & automate", body: "AI generates your blog, emails, and social schedule — publishing on autopilot so you can focus on your users." }, +]; + +function HowItWorks() { + return ( +
+ How it works +

+ Four phases.
One complete product. +

+
+ {PHASES.map((p, i) => ( +
+
+ {p.n} — {p.id} +
+
{p.title}
+

{p.body}

+
+ ))} +
+
+ ); +} + +// ─── Stats bar ───────────────────────────────────────────────────────────────── + +const STATS = [ + { n: "280+", label: "founders launched" }, + { n: "72h", label: "average time to first version" }, + { n: "4.9★", label: "average rating" }, + { n: "3×", label: "faster than hiring a developer" }, +]; + +function StatsBar() { + return ( +
+
+ {STATS.map((s, i) => ( +
0 ? 36 : 0, borderRight: i < 3 ? `1px solid ${T.border}` : "none" }}> +
{s.n}
+
{s.label}
+
+ ))} +
+
+ ); +} + +// ─── Empathy section ─────────────────────────────────────────────────────────── + +const PAINS = [ + { title: "No more \"I need to hire a developer first\"", body: "vibn is your developer. Start building the moment you have an idea." }, + { title: "No more staring at a blank marketing calendar", body: "AI generates and publishes your content every single week." }, + { title: "No more \"I'll launch when it's ready\"", body: "Most founders ship their first version in under 72 hours." }, +]; + +function EmpathySection() { + return ( +
+
+
+ Sound familiar? +

+ The idea is the hard part. Everything else shouldn't be. +

+

+ You know exactly what you want to build and who it's for. But the moment you think + about servers, databases, deployment pipelines, SEO strategies — the whole thing stalls. +

+

+ vibn exists to remove all of that. Not abstract it —{" "} + remove it entirely. +

+
+
+ {PAINS.map((p, i) => ( +
+
+
+
+
+
{p.title}
+
{p.body}
+
+
+ ))} +
+
+
+ ); +} + +// ─── Final CTA ───────────────────────────────────────────────────────────────── + +function FinalCta({ onCta }) { + return ( +
+

+ Your idea deserves to exist. +

+

+ Don't let the backend be the reason it doesn't. Start today — free, no code, no credit card. +

+ Build my product — free +
+ Joins 280+ non-technical founders already live +
+
+ ); +} + +// ─── Footer ──────────────────────────────────────────────────────────────────── + +function Footer() { + return ( +
+ vibn +
+ {["Product", "Pricing", "Stories", "Blog", "Privacy", "Terms"].map(l => ( + {l} + ))} +
+ © 2026 vibn +
+ ); +} + +// ─── Root export ─────────────────────────────────────────────────────────────── + +export default function Website({ onGetStarted = () => {}, onLogin = () => {} }) { + return ( +
+ +
+ ); +} diff --git a/justine/vibn-dashboard.jsx b/justine/vibn-dashboard.jsx new file mode 100644 index 0000000..50df402 --- /dev/null +++ b/justine/vibn-dashboard.jsx @@ -0,0 +1,484 @@ +// vibn — Projects Dashboard +// Restyled from original (DM Sans + purple/colour accents) → Ink & parchment +// Design: Lora serif + Inter sans, #1a1510 ink, #f7f4ee paper, no colour accent +// Usage: default export, no required props + +import { useState } from "react"; + +const T = { + ink: "#1a1510", + ink2: "#2c2c2a", + ink3: "#444441", + mid: "#5f5e5a", + muted: "#888780", + stone: "#b4b2a9", + parch: "#d3d1c7", + cream: "#f1efe8", + paper: "#f7f4ee", + white: "#fdfcfa", + border: "#e8e2d9", + border2:"#d3d1c7", +}; + +const F = { serif: "'Lora', Georgia, serif", sans: "'Inter', sans-serif" }; + +// ─── Shared primitives ───────────────────────────────────────────────────────── + +function StatusPill({ label, variant = "default" }) { + const styles = { + live: { bg: T.cream, text: T.ink3, border: T.border }, + building: { bg: T.cream, text: T.ink3, border: T.border }, + default: { bg: T.paper, text: T.muted, border: T.border }, + invoiced: { bg: T.ink, text: T.paper, border: T.ink }, + unbilled: { bg: T.cream, text: T.ink3, border: T.border }, + scheduled: { bg: T.parch, text: T.ink2, border: T.border2 }, + }; + const s = styles[variant] || styles.default; + return ( + {label} + ); +} + +function InkBtn({ children, onClick, small, outline }) { + return ( + + ); +} + +// ─── Nav ─────────────────────────────────────────────────────────────────────── + +function Nav({ screen, setScreen }) { + return ( + + ); +} + +// ─── Data ────────────────────────────────────────────────────────────────────── + +const PROJECTS = [ + { + id: "launchpad", label: "Launchpad", initial: "L", + type: "own", status: "live", url: "launchpad.vibn.app", + stats: { visitors: "2.4k", signups: 183, mrr: "$840" }, + }, + { + id: "flowmatic", label: "Flowmatic", initial: "F", + type: "client", status: "live", url: "flowmatic.app", + client: "Acme Corp", + stats: { visitors: "890", signups: 54, mrr: "$210" }, + costs: { total: 48.20, llm: 29.20, compute: 11.60, other: 7.40, billed: false }, + }, + { + id: "taskly", label: "Taskly", initial: "T", + type: "client", status: "building", url: null, + client: "Beta Labs", buildProgress: 60, + costs: { total: 12.40, llm: 9.20, compute: 3.20, other: 0, billed: false }, + }, +]; + +const ACTIVITY = [ + { text: "Launchpad — Blog post published:", detail: '"How to launch faster with AI"', time: "2h ago" }, + { text: "Flowmatic — New signup:", detail: "marcus@email.com", time: "4h ago" }, + { text: "Taskly — Checkout page built and deployed", detail: "", time: "6h ago" }, + { text: "Launchpad — Newsletter #12", detail: "scheduled", time: "Yesterday" }, +]; + +const BILLING_ROWS = [ + { label: "Flowmatic", initial: "F", client: "Acme Corp", llm: 29.20, compute: 11.60, other: 7.40, total: 48.20, billed: false }, + { label: "Taskly", initial: "T", client: "Beta Labs", llm: 9.20, compute: 3.20, other: 0, total: 12.40, billed: false }, + { label: "Flowmatic", initial: "F", client: "Acme · Feb", llm: 22.10, compute: 8.40, other: 4.20, total: 34.70, billed: true }, +]; + +const COST_LOG = [ + { time: "2h ago", desc: "LLM: Homepage copy generation", project: "Flowmatic", cost: 0.82 }, + { time: "3h ago", desc: "LLM: Checkout page code", project: "Taskly", cost: 1.24 }, + { time: "5h ago", desc: "LLM: Weekly newsletter draft", project: "Flowmatic", cost: 0.43 }, + { time: "6h ago", desc: "Compute: Build pipeline run", project: "Taskly", cost: 0.18 }, + { time: "8h ago", desc: "LLM: Discover phase Q&A", project: "Flowmatic", cost: 0.31 }, + { time: "Yesterday", desc: "Email delivery · 240 recipients", project: "Flowmatic", cost: 0.96 }, +]; + +// ─── Project card ────────────────────────────────────────────────────────────── + +function ProjectCard({ project }) { + const isClient = project.type === "client"; + const isBuilding = project.status === "building"; + + return ( +
+ + {/* Header preview */} + {isBuilding ? ( +
+
+
+ Build phase · {project.buildProgress}% complete +
+
+
+
+
+ {isClient && ( +
+ Client +
+ )} +
+ ) : ( +
+
+
+
+
+
+ {isClient ? "Client" : "My product"} +
+
+ )} + +
+ {/* Identity row */} +
+
+
+ {project.initial} +
+
+
{project.label}
+
+ {isClient ? `${project.client} · ` : ""} + {project.url || "Setting up pages…"} +
+
+
+ +
+ + {/* Cost strip — client + building */} + {isClient && project.costs && isBuilding && ( +
+
+
Costs so far
+
${project.costs.total.toFixed(2)}
+
+ +
+ )} + + {/* Cost strip — client + live */} + {isClient && project.costs && !isBuilding && ( +
+
+
Costs this month
+
${project.costs.total.toFixed(2)}
+
+
+ LLM ${project.costs.llm.toFixed(2)}
+ Compute ${project.costs.compute.toFixed(2)} +
+ {!project.costs.billed && ( + + )} +
+ )} + + {/* Stats */} + {!isBuilding && project.stats && ( +
+ {[["visitors", project.stats.visitors], ["signups", project.stats.signups], ["MRR", project.stats.mrr]].map(([k, v]) => ( +
+
{v}
+
{k}
+
+ ))} +
+ )} + + {/* Actions */} + {isBuilding ? ( + + ) : ( +
+ {[["⬡", "Build"], ["◈", "Grow"]].map(([icon, label]) => ( +
+ {icon} + {label} +
+ ))} +
+ ↗ +
+
+ )} +
+
+ ); +} + +// ─── Projects screen ─────────────────────────────────────────────────────────── + +function ProjectsScreen({ setScreen }) { + const totalUnbilled = PROJECTS + .filter(p => p.type === "client" && p.costs?.billed === false) + .reduce((s, p) => s + p.costs.total, 0); + + return ( +
+
+
+

Your projects

+

3 active · 1 building

+
+
+ {totalUnbilled > 0 && ( + + )} + + New project +
+
+ +
+ {PROJECTS.map(p => )} + + {/* New project CTA card */} +
e.currentTarget.style.background = T.cream} + onMouseLeave={e => e.currentTarget.style.background = "transparent"} + > +
+
+
+
New project
+
For yourself or a client
+
+
+
+ + {/* Activity feed */} +
+
Recent activity
+ {ACTIVITY.map((a, i) => ( +
+
+
+ {a.text}{" "}{a.detail && {a.detail}} +
+ {a.time} +
+ ))} +
+
+ ); +} + +// ─── Billing screen ──────────────────────────────────────────────────────────── + +function BillingScreen() { + const [tab, setTab] = useState("billing"); + const unbilled = BILLING_ROWS.filter(r => !r.billed).reduce((s, r) => s + r.total, 0); + + return ( +
+ + {/* Sub-tabs */} +
+ {[["billing", "Client billing"], ["costs", "Cost tracker"]].map(([id, label]) => ( + + ))} +
+ + {tab === "billing" && <> +
+
+

Client billing

+

All costs tracked and ready to invoice

+
+ Generate invoice +
+ +
+ {[ + { label: "Total unbilled", value: `$${unbilled.toFixed(2)}` }, + { label: "LLM costs", value: "$38.40" }, + { label: "Compute", value: "$14.80" }, + { label: "Other", value: "$7.40" }, + ].map(c => ( +
+
{c.label}
+
{c.value}
+
+ ))} +
+ +
+
+ Breakdown by client + +
+
+ {["Project / Client", "LLM", "Compute", "Other", "Total", "Status"].map(h => ( +
{h}
+ ))} +
+ {BILLING_ROWS.map((r, i) => ( +
+
+
{r.initial}
+
+
{r.label}
+
{r.client}
+
+
+
${r.llm.toFixed(2)}
+
${r.compute.toFixed(2)}
+
${r.other.toFixed(2)}
+
${r.total.toFixed(2)}
+
+ + {!r.billed && ( + + )} +
+
+ ))} +
+ } + + {tab === "costs" && <> +
+

Cost tracker

+

Every dollar spent, broken down by type and project

+
+ +
+
+
LLM usage
+ {[ + { label: "Code generation", amount: 21.40, pct: 56 }, + { label: "Content & marketing", amount: 10.20, pct: 27 }, + { label: "Chat assist", amount: 6.80, pct: 18 }, + ].map(r => ( +
+
+ {r.label} + ${r.amount.toFixed(2)} +
+
+
+
+
+ ))} +
+ Total LLM + $38.40 +
+
+ +
+
Infrastructure
+ {[ + { label: "Hosting & compute", amount: 11.60 }, + { label: "Database", amount: 3.20 }, + { label: "Email delivery", amount: 4.20 }, + { label: "Domain & SSL", amount: 3.20 }, + ].map(r => ( +
+ {r.label} + ${r.amount.toFixed(2)} +
+ ))} +
+ Total infra + $22.20 +
+
+
+ +
+
+ Recent charges +
+
+ {["Time", "Description", "Project", "Cost"].map(h => ( +
{h}
+ ))} +
+ {COST_LOG.map((row, i) => ( +
+
{row.time}
+
{row.desc}
+
{row.project}
+
${row.cost.toFixed(2)}
+
+ ))} +
+ } +
+ ); +} + +// ─── Root ────────────────────────────────────────────────────────────────────── + +export default function Dashboard() { + const [screen, setScreen] = useState("projects"); + + return ( +
+ +
+ ); +} diff --git a/justine/vibn-website.jsx b/justine/vibn-website.jsx new file mode 100644 index 0000000..9e5edc9 --- /dev/null +++ b/justine/vibn-website.jsx @@ -0,0 +1,288 @@ +// vibn — Marketing Website +// Design: Ink & parchment — Lora serif + Inter sans, no colour accent +// Usage: + +import { useState } from "react"; + +const T = { + ink: "#1a1510", + ink2: "#2c2c2a", + ink3: "#444441", + mid: "#5f5e5a", + muted: "#888780", + stone: "#b4b2a9", + parch: "#d3d1c7", + cream: "#f1efe8", + paper: "#f7f4ee", + white: "#fdfcfa", + border: "#e8e2d9", +}; + +const F = { serif: "'Lora', Georgia, serif", sans: "'Inter', sans-serif" }; + +// ─── Primitives ──────────────────────────────────────────────────────────────── + +function Eyebrow({ children }) { + return ( +
+ {children} +
+ ); +} + +function PrimaryBtn({ children, onClick, large }) { + return ( + + ); +} + +// ─── Nav ─────────────────────────────────────────────────────────────────────── + +function Nav({ onGetStarted, onLogin }) { + return ( + + ); +} + +// ─── Hero ────────────────────────────────────────────────────────────────────── + +function Hero({ onCta }) { + return ( +
+ For non-technical founders +

+ You have the idea.
+ We handle
+ everything else. +

+

+ No backend. No DevOps. No marketing agency. Describe your idea and vibn + builds, deploys, and promotes it — automatically. +

+
+ Start free — no code needed + ★★★★★  280 founders launched +
+

No credit card required · Free forever plan

+
+ ); +} + +// ─── Quote band ──────────────────────────────────────────────────────────────── + +const QUOTES = [ + { q: "I had the idea for 2 years. The backend terrified me. vibn shipped it in 4 days and handles all my marketing.", by: "Alex K.", role: "Founder, Taskly" }, + { q: "I have zero coding experience. Three weeks in I have 300 paying users. That's entirely because of vibn.", by: "Marcus L.", role: "Founder, Flowmatic" }, + { q: "The marketing autopilot alone saved me ten hours a week. My blog runs itself. I just focus on my product.", by: "Sara R.", role: "Founder, Nudge" }, +]; + +function QuoteBand() { + return ( +
+
+ {QUOTES.map((q, i) => ( +
+
+
+

"{q.q}"

+ — {q.by}, {q.role} +
+
+ ))} +
+
+ ); +} + +// ─── How it works ────────────────────────────────────────────────────────────── + +const PHASES = [ + { n: "01", id: "Discover", title: "Define your idea", body: "Six guided questions turn a rough idea into a full product plan — pages, architecture, revenue model. No jargon." }, + { n: "02", id: "Design", title: "Choose your style", body: "Pick a visual style and see your exact site and emails live before a single line of code is written." }, + { n: "03", id: "Build", title: "Your app, live", body: "AI writes every line. Auth, database, payments, all pages — deployed and live. Describe changes in plain English." }, + { n: "04", id: "Grow", title: "Market & automate", body: "AI generates your blog, emails, and social schedule — publishing on autopilot so you can focus on your users." }, +]; + +function HowItWorks() { + return ( +
+ How it works +

+ Four phases.
One complete product. +

+
+ {PHASES.map((p, i) => ( +
+
+ {p.n} — {p.id} +
+
{p.title}
+

{p.body}

+
+ ))} +
+
+ ); +} + +// ─── Stats bar ───────────────────────────────────────────────────────────────── + +const STATS = [ + { n: "280+", label: "founders launched" }, + { n: "72h", label: "average time to first version" }, + { n: "4.9★", label: "average rating" }, + { n: "3×", label: "faster than hiring a developer" }, +]; + +function StatsBar() { + return ( +
+
+ {STATS.map((s, i) => ( +
0 ? 36 : 0, borderRight: i < 3 ? `1px solid ${T.border}` : "none" }}> +
{s.n}
+
{s.label}
+
+ ))} +
+
+ ); +} + +// ─── Empathy section ─────────────────────────────────────────────────────────── + +const PAINS = [ + { title: "No more \"I need to hire a developer first\"", body: "vibn is your developer. Start building the moment you have an idea." }, + { title: "No more staring at a blank marketing calendar", body: "AI generates and publishes your content every single week." }, + { title: "No more \"I'll launch when it's ready\"", body: "Most founders ship their first version in under 72 hours." }, +]; + +function EmpathySection() { + return ( +
+
+
+ Sound familiar? +

+ The idea is the hard part. Everything else shouldn't be. +

+

+ You know exactly what you want to build and who it's for. But the moment you think + about servers, databases, deployment pipelines, SEO strategies — the whole thing stalls. +

+

+ vibn exists to remove all of that. Not abstract it —{" "} + remove it entirely. +

+
+
+ {PAINS.map((p, i) => ( +
+
+
+
+
+
{p.title}
+
{p.body}
+
+
+ ))} +
+
+
+ ); +} + +// ─── Final CTA ───────────────────────────────────────────────────────────────── + +function FinalCta({ onCta }) { + return ( +
+

+ Your idea deserves to exist. +

+

+ Don't let the backend be the reason it doesn't. Start today — free, no code, no credit card. +

+ Build my product — free +
+ Joins 280+ non-technical founders already live +
+
+ ); +} + +// ─── Footer ──────────────────────────────────────────────────────────────────── + +function Footer() { + return ( +
+ vibn +
+ {["Product", "Pricing", "Stories", "Blog", "Privacy", "Terms"].map(l => ( + {l} + ))} +
+ © 2026 vibn +
+ ); +} + +// ─── Root export ─────────────────────────────────────────────────────────────── + +export default function Website({ onGetStarted = () => {}, onLogin = () => {} }) { + return ( +
+ +
+ ); +} diff --git a/master-ai.code-workspace b/master-ai.code-workspace new file mode 100644 index 0000000..cfd12b8 --- /dev/null +++ b/master-ai.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../Downloads/vibn-screens" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/platform/backend/control-plane/package.json b/platform/backend/control-plane/package.json deleted file mode 100644 index 0a9afc1..0000000 --- a/platform/backend/control-plane/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@productos/control-plane", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "dist/index.js", - "scripts": { - "dev": "tsx watch src/index.ts", - "build": "tsc -p tsconfig.json", - "start": "node dist/index.js", - "lint": "eslint ." - }, - "dependencies": { - "@google-cloud/firestore": "^7.11.0", - "@google-cloud/storage": "^7.14.0", - "@fastify/cors": "^10.0.0", - "@fastify/helmet": "^13.0.0", - "@fastify/rate-limit": "^10.0.0", - "@fastify/sensible": "^6.0.0", - "fastify": "^5.0.0", - "zod": "^3.23.8", - "nanoid": "^5.0.7" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "tsx": "^4.19.0", - "typescript": "^5.5.4", - "eslint": "^9.8.0" - } -} diff --git a/platform/backend/control-plane/src/auth.ts b/platform/backend/control-plane/src/auth.ts deleted file mode 100644 index 9ee48db..0000000 --- a/platform/backend/control-plane/src/auth.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { FastifyRequest } from "fastify"; -import { config } from "./config.js"; - -/** - * V1: dev mode = trust caller. - * V2: validate Google OAuth/IAP identity token. - */ -export async function requireAuth(req: FastifyRequest) { - if (config.authMode === "dev") return; - throw new Error("AUTH_MODE oauth not yet implemented"); -} diff --git a/platform/backend/control-plane/src/config.ts b/platform/backend/control-plane/src/config.ts deleted file mode 100644 index 60f01ec..0000000 --- a/platform/backend/control-plane/src/config.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const config = { - port: Number(process.env.PORT ?? 8080), - projectId: process.env.GCP_PROJECT_ID ?? "productos-local", - artifactsBucket: process.env.GCS_BUCKET_ARTIFACTS ?? "productos-artifacts-local", - runsCollection: process.env.FIRESTORE_COLLECTION_RUNS ?? "runs", - toolsCollection: process.env.FIRESTORE_COLLECTION_TOOLS ?? "tools", - authMode: process.env.AUTH_MODE ?? "dev", - // Use in-memory storage when STORAGE_MODE=memory or when no GCP project is configured - storageMode: process.env.STORAGE_MODE ?? (process.env.GCP_PROJECT_ID ? "gcp" : "memory"), - - // Gitea - giteaUrl: process.env.GITEA_URL ?? "https://git.vibnai.com", - giteaToken: process.env.GITEA_TOKEN ?? "", - - // Coolify - coolifyUrl: process.env.COOLIFY_URL ?? "http://localhost:8000", - coolifyToken: process.env.COOLIFY_TOKEN ?? "", - - // Platform webhook base (used when registering Gitea webhooks) - platformUrl: process.env.PLATFORM_URL ?? "http://localhost:8080", -}; diff --git a/platform/backend/control-plane/src/coolify.ts b/platform/backend/control-plane/src/coolify.ts deleted file mode 100644 index 5a28783..0000000 --- a/platform/backend/control-plane/src/coolify.ts +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Coolify API integration - * - * Handles project creation, per-app service provisioning, and deployment - * triggering. Each app in a user's Turborepo monorepo gets its own - * Coolify service with the correct Turbo build filter set. - */ - -import { config } from "./config.js"; - -const DEFAULT_SERVER_UUID = process.env.COOLIFY_SERVER_UUID ?? "0"; - -async function coolifyFetch(path: string, options: RequestInit = {}): Promise { - const url = `${config.coolifyUrl}/api/v1${path}`; - const res = await fetch(url, { - ...options, - headers: { - "Authorization": `Bearer ${config.coolifyToken}`, - "Content-Type": "application/json", - ...options.headers, - }, - }); - return res; -} - -export async function createProject(name: string, description: string): Promise { - const res = await coolifyFetch("/projects", { - method: "POST", - body: JSON.stringify({ name, description }), - }); - - if (!res.ok) { - const body = await res.text(); - throw new Error(`Failed to create Coolify project: ${res.status} ${body}`); - } - - const data = await res.json() as { uuid: string }; - return data.uuid; -} - -type CreateServiceOptions = { - coolifyProjectUuid: string; - appName: string; - repoUrl: string; - repoBranch?: string; - domain: string; -}; - -/** - * Create a Coolify application service for one app within the monorepo. - * The build command uses turbo --filter so only the relevant app builds. - */ -export async function createAppService(opts: CreateServiceOptions): Promise { - const { - coolifyProjectUuid, - appName, - repoUrl, - repoBranch = "main", - domain, - } = opts; - - const res = await coolifyFetch("/applications/public", { - method: "POST", - body: JSON.stringify({ - project_uuid: coolifyProjectUuid, - server_uuid: DEFAULT_SERVER_UUID, - name: appName, - git_repository: repoUrl, - git_branch: repoBranch, - build_command: `pnpm install && turbo run build --filter=${appName}`, - start_command: `turbo run start --filter=${appName}`, - publish_directory: `apps/${appName}/.next`, - fqdn: `https://${domain}`, - environment_variables: [], - }), - }); - - if (!res.ok) { - const body = await res.text(); - throw new Error(`Failed to create Coolify service for ${appName}: ${res.status} ${body}`); - } - - const data = await res.json() as { uuid: string }; - return data.uuid; -} - -export async function triggerDeploy(serviceUuid: string): Promise { - const res = await coolifyFetch(`/applications/${serviceUuid}/deploy`, { - method: "POST", - }); - - if (!res.ok) { - const body = await res.text(); - throw new Error(`Failed to trigger deploy for ${serviceUuid}: ${res.status} ${body}`); - } - - const data = await res.json() as { deployment_uuid: string }; - return data.deployment_uuid; -} - -export async function getDeploymentStatus(deploymentUuid: string): Promise { - const res = await coolifyFetch(`/deployments/${deploymentUuid}`); - if (!res.ok) return "unknown"; - const data = await res.json() as { status: string }; - return data.status; -} - -export async function setEnvVars( - serviceUuid: string, - vars: Record -): Promise { - for (const [key, value] of Object.entries(vars)) { - await coolifyFetch(`/applications/${serviceUuid}/envs`, { - method: "POST", - body: JSON.stringify({ key, value, is_preview: false }), - }); - } -} diff --git a/platform/backend/control-plane/src/gemini.ts b/platform/backend/control-plane/src/gemini.ts deleted file mode 100644 index e836ba7..0000000 --- a/platform/backend/control-plane/src/gemini.ts +++ /dev/null @@ -1,410 +0,0 @@ -/** - * Gemini Integration for Product OS - * - * Supports: - * - Chat completions with streaming - * - Tool/function calling - * - Context-aware responses - * - * Set GOOGLE_CLOUD_PROJECT and optionally GEMINI_MODEL env vars. - * For local dev without Vertex AI, set GEMINI_API_KEY for AI Studio. - */ - -import { config } from "./config.js"; - -export interface ChatMessage { - role: "user" | "assistant" | "system"; - content: string; -} - -export interface ToolCall { - name: string; - arguments: Record; -} - -export interface ChatResponse { - message: string; - toolCalls?: ToolCall[]; - finishReason: "stop" | "tool_calls" | "error"; -} - -// Tool definitions that Gemini can call -export const PRODUCT_OS_TOOLS = [ - { - name: "deploy_service", - description: "Deploy a Cloud Run service. Use when user wants to deploy, ship, or launch code.", - parameters: { - type: "object", - properties: { - service_name: { type: "string", description: "Name of the service to deploy" }, - repo: { type: "string", description: "Git repository URL" }, - ref: { type: "string", description: "Git branch, tag, or commit" }, - env: { type: "string", enum: ["dev", "staging", "prod"], description: "Target environment" } - }, - required: ["service_name"] - } - }, - { - name: "get_funnel_analytics", - description: "Get funnel conversion metrics. Use when user asks about funnels, conversions, or drop-offs.", - parameters: { - type: "object", - properties: { - range_days: { type: "integer", description: "Number of days to analyze", default: 30 } - } - } - }, - { - name: "get_top_drivers", - description: "Identify top factors driving a metric. Use when user asks why something changed or what drives conversions.", - parameters: { - type: "object", - properties: { - metric: { type: "string", description: "The metric to analyze (e.g., 'conversion', 'retention')" }, - range_days: { type: "integer", description: "Number of days to analyze", default: 30 } - }, - required: ["metric"] - } - }, - { - name: "generate_marketing_posts", - description: "Generate social media posts for a campaign. Use when user wants to create marketing content.", - parameters: { - type: "object", - properties: { - goal: { type: "string", description: "Campaign goal (e.g., 'launch announcement')" }, - product: { type: "string", description: "Product or feature name" }, - channels: { - type: "array", - items: { type: "string" }, - description: "Social channels (e.g., ['x', 'linkedin'])" - } - }, - required: ["goal"] - } - }, - { - name: "get_service_status", - description: "Check the status of a deployed service. Use when user asks about service health or deployment status.", - parameters: { - type: "object", - properties: { - service_name: { type: "string", description: "Name of the service" }, - region: { type: "string", description: "GCP region", default: "us-central1" } - }, - required: ["service_name"] - } - }, - { - name: "generate_code", - description: "Generate or modify code. Use when user asks to write, fix, refactor, or change code.", - parameters: { - type: "object", - properties: { - task: { type: "string", description: "What code change to make" }, - file_path: { type: "string", description: "Target file path (if known)" }, - language: { type: "string", description: "Programming language" }, - context: { type: "string", description: "Additional context about the codebase" } - }, - required: ["task"] - } - }, - { - name: "deploy_app", - description: "Deploy a specific app from the project monorepo. Use when user wants to deploy or ship one of their apps (product, website, admin, storybook).", - parameters: { - type: "object", - properties: { - project_id: { type: "string", description: "The project ID" }, - app_name: { - type: "string", - enum: ["product", "website", "admin", "storybook"], - description: "Which app to deploy" - }, - env: { - type: "string", - enum: ["dev", "staging", "prod"], - description: "Target environment" - } - }, - required: ["project_id", "app_name"] - } - }, - { - name: "scaffold_app", - description: "Add a new app to the project monorepo. Use when user wants to add a new application beyond the defaults.", - parameters: { - type: "object", - properties: { - project_id: { type: "string", description: "The project ID" }, - app_name: { type: "string", description: "Name for the new app (e.g. 'mobile', 'api', 'dashboard')" }, - framework: { - type: "string", - enum: ["nextjs", "astro", "express", "fastify"], - description: "Framework to scaffold" - } - }, - required: ["project_id", "app_name"] - } - } -]; - -// System prompt for Product OS assistant -const SYSTEM_PROMPT = `You are the AI for a software platform where every project is a Turborepo monorepo containing multiple apps: product, website, admin, and storybook. You have full visibility and control over the entire project. - -Each project has: -- apps/product — the core user-facing application -- apps/website — the marketing and landing site -- apps/admin — internal admin tooling -- apps/storybook — component browser and design system -- packages/ui — shared React component library -- packages/tokens — shared design tokens (colors, spacing, typography) -- packages/types — shared TypeScript types -- packages/config — shared eslint and tsconfig - -You can help with: -- Deploying any app using turbo run build --filter= -- Writing and modifying code across any app or package in the monorepo -- Adding new apps or packages to the project -- Analyzing product metrics and funnels -- Generating marketing content -- Understanding what drives user behavior - -When a user says "deploy" without specifying an app, ask which one or default to "product". -When a user asks to change something visual, consider whether it belongs in packages/ui or packages/tokens. -When users ask you to do something, use the available tools to take action. Be concise and specific about which app or package you are working in.`; - -/** - * Chat with Gemini - * Uses Vertex AI in production, or AI Studio API key for local dev - */ -export async function chat( - messages: ChatMessage[], - options: { stream?: boolean } = {} -): Promise { - const apiKey = process.env.GEMINI_API_KEY; - const projectId = config.projectId; - const model = process.env.GEMINI_MODEL ?? "gemini-1.5-flash"; - - // Build the request - const contents = [ - { role: "user", parts: [{ text: SYSTEM_PROMPT }] }, - { role: "model", parts: [{ text: "Understood. I'm Product OS, ready to help you build and operate your SaaS product. How can I help?" }] }, - ...messages.map(m => ({ - role: m.role === "assistant" ? "model" : "user", - parts: [{ text: m.content }] - })) - ]; - - const tools = [{ - functionDeclarations: PRODUCT_OS_TOOLS - }]; - - // Use AI Studio API if API key is set (local dev) - if (apiKey) { - return chatWithAIStudio(apiKey, model, contents, tools); - } - - // Use Vertex AI if project is set (production) - if (projectId && projectId !== "productos-local") { - return chatWithVertexAI(projectId, model, contents, tools); - } - - // Mock response for local dev without credentials - return mockChat(messages); -} - -async function chatWithAIStudio( - apiKey: string, - model: string, - contents: any[], - tools: any[] -): Promise { - const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; - - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - contents, - tools, - generationConfig: { - temperature: 0.7, - maxOutputTokens: 2048 - } - }) - }); - - if (!response.ok) { - const error = await response.text(); - console.error("Gemini API error:", error); - throw new Error(`Gemini API error: ${response.status}`); - } - - const data = await response.json(); - return parseGeminiResponse(data); -} - -async function chatWithVertexAI( - projectId: string, - model: string, - contents: any[], - tools: any[] -): Promise { - // Vertex AI endpoint - const location = process.env.VERTEX_LOCATION ?? "us-central1"; - const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${location}/publishers/google/models/${model}:generateContent`; - - // Get access token (requires gcloud auth) - const { GoogleAuth } = await import("google-auth-library"); - const auth = new GoogleAuth({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }); - const client = await auth.getClient(); - const token = await client.getAccessToken(); - - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${token.token}` - }, - body: JSON.stringify({ - contents, - tools, - generationConfig: { - temperature: 0.7, - maxOutputTokens: 2048 - } - }) - }); - - if (!response.ok) { - const error = await response.text(); - console.error("Vertex AI error:", error); - throw new Error(`Vertex AI error: ${response.status}`); - } - - const data = await response.json(); - return parseGeminiResponse(data); -} - -function parseGeminiResponse(data: any): ChatResponse { - const candidate = data.candidates?.[0]; - if (!candidate) { - return { message: "No response from Gemini", finishReason: "error" }; - } - - const content = candidate.content; - const parts = content?.parts ?? []; - - // Check for function calls - const functionCalls = parts.filter((p: any) => p.functionCall); - if (functionCalls.length > 0) { - const toolCalls = functionCalls.map((p: any) => ({ - name: p.functionCall.name, - arguments: p.functionCall.args ?? {} - })); - return { - message: "", - toolCalls, - finishReason: "tool_calls" - }; - } - - // Regular text response - const text = parts.map((p: any) => p.text ?? "").join(""); - return { - message: text, - finishReason: "stop" - }; -} - -/** - * Mock chat for local development without Gemini credentials - */ -function mockChat(messages: ChatMessage[]): ChatResponse { - const lastMessage = messages[messages.length - 1]?.content.toLowerCase() ?? ""; - - // Check marketing/campaign FIRST (before deploy) since "launch" can be ambiguous - if (lastMessage.includes("marketing") || lastMessage.includes("campaign") || lastMessage.includes("post") || - (lastMessage.includes("launch") && !lastMessage.includes("deploy"))) { - return { - message: "", - toolCalls: [{ - name: "generate_marketing_posts", - arguments: { goal: "product launch", channels: ["x", "linkedin"] } - }], - finishReason: "tool_calls" - }; - } - - // Simple keyword matching to simulate tool calls - if (lastMessage.includes("deploy") || lastMessage.includes("ship") || lastMessage.includes("staging") || lastMessage.includes("production")) { - return { - message: "", - toolCalls: [{ - name: "deploy_service", - arguments: { service_name: "my-service", env: lastMessage.includes("staging") ? "staging" : lastMessage.includes("prod") ? "prod" : "dev" } - }], - finishReason: "tool_calls" - }; - } - - if (lastMessage.includes("funnel") || lastMessage.includes("conversion") || lastMessage.includes("analytics")) { - return { - message: "", - toolCalls: [{ - name: "get_funnel_analytics", - arguments: { range_days: 30 } - }], - finishReason: "tool_calls" - }; - } - - if (lastMessage.includes("why") || lastMessage.includes("driver") || lastMessage.includes("cause")) { - return { - message: "", - toolCalls: [{ - name: "get_top_drivers", - arguments: { metric: "conversion", range_days: 30 } - }], - finishReason: "tool_calls" - }; - } - - if (lastMessage.includes("status") || lastMessage.includes("health")) { - return { - message: "", - toolCalls: [{ - name: "get_service_status", - arguments: { service_name: "my-service" } - }], - finishReason: "tool_calls" - }; - } - - if (lastMessage.includes("code") || lastMessage.includes("function") || lastMessage.includes("write") || lastMessage.includes("create")) { - return { - message: "", - toolCalls: [{ - name: "generate_code", - arguments: { task: lastMessage, language: "typescript" } - }], - finishReason: "tool_calls" - }; - } - - // Default response - return { - message: `I'm Product OS, your AI assistant for building and operating SaaS products. I can help you: - -• **Deploy** services to Cloud Run -• **Analyze** funnel metrics and conversions -• **Generate** marketing content -• **Understand** what drives user behavior - -What would you like to do? - -_(Note: Running in mock mode - set GEMINI_API_KEY for real AI responses)_`, - finishReason: "stop" - }; -} diff --git a/platform/backend/control-plane/src/gitea.ts b/platform/backend/control-plane/src/gitea.ts deleted file mode 100644 index d4680e1..0000000 --- a/platform/backend/control-plane/src/gitea.ts +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Gitea API integration - * - * Handles repo creation, file scaffolding, and webhook registration - * for user projects. All project repos are created under the user's - * Gitea account and contain the full Turborepo monorepo structure. - */ - -import { config } from "./config.js"; -import { readdir, readFile } from "node:fs/promises"; -import { join, relative } from "node:path"; -import { fileURLToPath } from "node:url"; -import { dirname } from "node:path"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const TEMPLATES_DIR = join(__dirname, "../../../../scripts/templates/turborepo"); - -type GiteaFile = { - path: string; - content: string; -}; - -async function giteaFetch(path: string, options: RequestInit = {}): Promise { - const url = `${config.giteaUrl}/api/v1${path}`; - const res = await fetch(url, { - ...options, - headers: { - "Authorization": `token ${config.giteaToken}`, - "Content-Type": "application/json", - ...options.headers, - }, - }); - return res; -} - -export async function createRepo(owner: string, repoName: string, description: string): Promise { - const res = await giteaFetch(`/user/repos`, { - method: "POST", - body: JSON.stringify({ - name: repoName, - description, - private: false, - auto_init: false, - }), - }); - - if (!res.ok) { - const body = await res.text(); - throw new Error(`Failed to create Gitea repo: ${res.status} ${body}`); - } - - const data = await res.json() as { clone_url: string }; - return data.clone_url; -} - -export async function registerWebhook(owner: string, repoName: string, webhookUrl: string): Promise { - const res = await giteaFetch(`/repos/${owner}/${repoName}/hooks`, { - method: "POST", - body: JSON.stringify({ - type: "gitea", - active: true, - events: ["push", "pull_request"], - config: { - url: webhookUrl, - content_type: "json", - }, - }), - }); - - if (!res.ok) { - const body = await res.text(); - throw new Error(`Failed to register webhook: ${res.status} ${body}`); - } -} - -/** - * Walk the template directory and collect all files with their content, - * replacing {{project-slug}} and {{project-name}} placeholders. - */ -async function collectTemplateFiles( - projectSlug: string, - projectName: string -): Promise { - const files: GiteaFile[] = []; - - async function walk(dir: string) { - const entries = await readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = join(dir, entry.name); - if (entry.isDirectory()) { - await walk(fullPath); - } else { - const relPath = relative(TEMPLATES_DIR, fullPath); - let content = await readFile(fullPath, "utf-8"); - content = content - .replaceAll("{{project-slug}}", projectSlug) - .replaceAll("{{project-name}}", projectName); - files.push({ path: relPath, content }); - } - } - } - - await walk(TEMPLATES_DIR); - return files; -} - -/** - * Push the full Turborepo scaffold to a Gitea repo as an initial commit. - * Uses Gitea's contents API to create each file individually. - */ -export async function scaffoldRepo( - owner: string, - repoName: string, - projectSlug: string, - projectName: string -): Promise { - const files = await collectTemplateFiles(projectSlug, projectName); - - for (const file of files) { - const encoded = Buffer.from(file.content).toString("base64"); - const res = await giteaFetch(`/repos/${owner}/${repoName}/contents/${file.path}`, { - method: "POST", - body: JSON.stringify({ - message: `chore: scaffold ${file.path}`, - content: encoded, - branch: "main", - }), - }); - - if (!res.ok) { - const body = await res.text(); - throw new Error(`Failed to push ${file.path}: ${res.status} ${body}`); - } - } -} - -export async function getRepoTree(owner: string, repoName: string, ref = "main"): Promise { - const res = await giteaFetch(`/repos/${owner}/${repoName}/git/trees/${ref}?recursive=true`); - if (!res.ok) return []; - const data = await res.json() as { tree: { path: string; type: string }[] }; - return data.tree.filter(e => e.type === "blob").map(e => e.path); -} - -export async function getFileContent( - owner: string, - repoName: string, - filePath: string, - ref = "main" -): Promise { - const res = await giteaFetch(`/repos/${owner}/${repoName}/contents/${filePath}?ref=${ref}`); - if (!res.ok) return null; - const data = await res.json() as { content: string }; - return Buffer.from(data.content, "base64").toString("utf-8"); -} diff --git a/platform/backend/control-plane/src/index.ts b/platform/backend/control-plane/src/index.ts deleted file mode 100644 index 6ccda0f..0000000 --- a/platform/backend/control-plane/src/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Fastify from "fastify"; -import cors from "@fastify/cors"; -import helmet from "@fastify/helmet"; -import rateLimit from "@fastify/rate-limit"; -import sensible from "@fastify/sensible"; -import { config } from "./config.js"; -import { healthRoutes } from "./routes/health.js"; -import { toolRoutes } from "./routes/tools.js"; -import { runRoutes } from "./routes/runs.js"; -import { chatRoutes } from "./routes/chat.js"; -import { projectRoutes } from "./routes/projects.js"; - -const app = Fastify({ logger: true }); - -await app.register(cors, { origin: true }); -await app.register(helmet); -await app.register(sensible); -await app.register(rateLimit, { max: 300, timeWindow: "1 minute" }); - -await app.register(healthRoutes); -await app.register(toolRoutes); -await app.register(runRoutes); -await app.register(chatRoutes); -await app.register(projectRoutes); - -app.listen({ port: config.port, host: "0.0.0.0" }).then(() => { - console.log(`🚀 Control Plane API running on http://localhost:${config.port}`); -}).catch((err) => { - app.log.error(err); - process.exit(1); -}); diff --git a/platform/backend/control-plane/src/registry.ts b/platform/backend/control-plane/src/registry.ts deleted file mode 100644 index bb10752..0000000 --- a/platform/backend/control-plane/src/registry.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ToolDef } from "./types.js"; -import { listTools } from "./storage/index.js"; - -/** - * Simple registry. V2: cache + versioning + per-tenant overrides. - */ -export async function getRegistry(): Promise> { - const tools = await listTools(); - return Object.fromEntries(tools.map(t => [t.name, t])); -} diff --git a/platform/backend/control-plane/src/routes/chat.ts b/platform/backend/control-plane/src/routes/chat.ts deleted file mode 100644 index 54436bc..0000000 --- a/platform/backend/control-plane/src/routes/chat.ts +++ /dev/null @@ -1,331 +0,0 @@ -import type { FastifyInstance } from "fastify"; -import { requireAuth } from "../auth.js"; -import { chat, ChatMessage, ChatResponse, ToolCall } from "../gemini.js"; -import { getRegistry } from "../registry.js"; -import { saveRun, writeArtifactText, getProject } from "../storage/index.js"; -import { nanoid } from "nanoid"; -import type { RunRecord } from "../types.js"; - -interface ChatRequest { - messages: ChatMessage[]; - project_id?: string; - context?: { - files?: { path: string; content: string }[]; - selection?: { path: string; text: string; startLine: number }; - }; - autoExecuteTools?: boolean; -} - -interface ChatResponseWithRuns extends ChatResponse { - runs?: RunRecord[]; -} - -export async function chatRoutes(app: FastifyInstance) { - /** - * Chat endpoint - proxies to Gemini with tool calling support - */ - app.post<{ Body: ChatRequest }>("/chat", async (req): Promise => { - await requireAuth(req); - - const { messages, project_id, context, autoExecuteTools = true } = req.body; - - let enhancedMessages = [...messages]; - - // Inject project context so the AI understands the full monorepo structure - if (project_id) { - const project = await getProject(project_id); - if (project) { - const appList = project.apps.map(a => ` - ${a.name} (${a.path})${a.domain ? ` → ${a.domain}` : ""}`).join("\n"); - const projectContext = [ - `Project: ${project.name} (${project.slug})`, - `Repo: ${project.repo || "provisioning..."}`, - `Status: ${project.status}`, - `Apps in this monorepo:`, - appList, - `Shared packages: ui, tokens, types, config`, - `Build system: Turborepo ${project.turboVersion}`, - `Build command: turbo run build --filter=`, - ].join("\n"); - - enhancedMessages = [ - { role: "user" as const, content: `Project context:\n${projectContext}` }, - ...messages, - ]; - } - } - - // Enhance messages with file/selection context if provided - if (context?.files?.length) { - const fileContext = context.files - .map(f => `File: ${f.path}\n\`\`\`\n${f.content}\n\`\`\``) - .join("\n\n"); - - enhancedMessages = [ - { role: "user" as const, content: `Context:\n${fileContext}` }, - ...enhancedMessages, - ]; - } - - if (context?.selection) { - const selectionContext = `Selected code in ${context.selection.path} (line ${context.selection.startLine}):\n\`\`\`\n${context.selection.text}\n\`\`\``; - enhancedMessages = [ - { role: "user" as const, content: selectionContext }, - ...enhancedMessages, - ]; - } - - // Call Gemini - const response = await chat(enhancedMessages); - - // If tool calls and auto-execute is enabled, run them - if (response.toolCalls && response.toolCalls.length > 0 && autoExecuteTools) { - const runs = await executeToolCalls(response.toolCalls, req.body); - - // Generate a summary of what was done - const summary = generateToolSummary(response.toolCalls, runs); - - return { - message: summary, - toolCalls: response.toolCalls, - runs, - finishReason: "tool_calls" - }; - } - - return response; - }); - - /** - * Streaming chat endpoint (SSE) - */ - app.get("/chat/stream", async (req, reply) => { - await requireAuth(req); - - reply.raw.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "Connection": "keep-alive" - }); - - // For now, return a message that streaming is not yet implemented - reply.raw.write(`data: ${JSON.stringify({ message: "Streaming not yet implemented", finishReason: "stop" })}\n\n`); - reply.raw.end(); - }); -} - -/** - * Execute tool calls by routing to the appropriate executor - */ -async function executeToolCalls( - toolCalls: ToolCall[], - request: ChatRequest -): Promise { - const runs: RunRecord[] = []; - const registry = await getRegistry(); - - for (const toolCall of toolCalls) { - // Map tool call names to actual tools - const toolMapping: Record = { - "deploy_service": "cloudrun.deploy_service", - "get_funnel_analytics": "analytics.funnel_summary", - "get_top_drivers": "analytics.top_drivers", - "generate_marketing_posts": "marketing.generate_channel_posts", - "get_service_status": "cloudrun.get_service_status", - "generate_code": "code.generate" // This one is special - handled inline - }; - - const actualToolName = toolMapping[toolCall.name]; - - // Special handling for code generation - if (toolCall.name === "generate_code") { - const codeRun = await handleCodeGeneration(toolCall.arguments); - runs.push(codeRun); - continue; - } - - const tool = actualToolName ? registry[actualToolName] : null; - - if (!tool) { - console.log(`Tool not found: ${toolCall.name} (mapped to ${actualToolName})`); - continue; - } - - // Create a run - const runId = `run_${new Date().toISOString().replace(/[-:.TZ]/g, "")}_${nanoid(8)}`; - const now = new Date().toISOString(); - - const run: RunRecord = { - run_id: runId, - tenant_id: "t_chat", - tool: actualToolName, - status: "queued", - created_at: now, - updated_at: now, - input: toolCall.arguments, - artifacts: { bucket: "", prefix: `runs/${runId}` } - }; - - await saveRun(run); - - // Execute the tool - try { - run.status = "running"; - run.updated_at = new Date().toISOString(); - await saveRun(run); - - const execUrl = `${tool.executor.url}${tool.executor.path}`; - const response = await fetch(execUrl, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - run_id: runId, - tenant_id: "t_chat", - input: toolCall.arguments - }) - }); - - if (!response.ok) { - throw new Error(`Executor error: ${response.status}`); - } - - const output = await response.json(); - run.status = "succeeded"; - run.output = output; - run.updated_at = new Date().toISOString(); - await saveRun(run); - - } catch (e: any) { - run.status = "failed"; - run.error = { message: e.message }; - run.updated_at = new Date().toISOString(); - await saveRun(run); - } - - runs.push(run); - } - - return runs; -} - -/** - * Handle code generation specially - */ -async function handleCodeGeneration(args: any): Promise { - const runId = `run_${new Date().toISOString().replace(/[-:.TZ]/g, "")}_${nanoid(8)}`; - const now = new Date().toISOString(); - - // For now, return a mock code generation result - // In production, this would call Gemini again with a code-specific prompt - const mockDiff = `--- a/${args.file_path || "src/example.ts"} -+++ b/${args.file_path || "src/example.ts"} -@@ -1,3 +1,10 @@ -+// Generated by Product OS -+// Task: ${args.task} -+ - export function example() { -- return "hello"; -+ // TODO: Implement ${args.task} -+ return { -+ status: "generated", -+ task: "${args.task}" -+ }; - }`; - - const run: RunRecord = { - run_id: runId, - tenant_id: "t_chat", - tool: "code.generate", - status: "succeeded", - created_at: now, - updated_at: now, - input: args, - output: { - type: "code_generation", - diff: mockDiff, - file_path: args.file_path || "src/example.ts", - language: args.language || "typescript", - description: `Generated code for: ${args.task}` - } - }; - - await saveRun(run); - await writeArtifactText(`runs/${runId}`, "diff.patch", mockDiff); - - return run; -} - -/** - * Generate a human-readable summary of tool executions - */ -function generateToolSummary(toolCalls: ToolCall[], runs: RunRecord[]): string { - const parts: string[] = []; - - for (let i = 0; i < toolCalls.length; i++) { - const tool = toolCalls[i]; - const run = runs[i]; - - if (!run) continue; - - const status = run.status === "succeeded" ? "✅" : "❌"; - - switch (tool.name) { - case "deploy_service": - if (run.status === "succeeded") { - const output = run.output as any; - parts.push(`${status} **Deployed** \`${tool.arguments.service_name}\` to ${tool.arguments.env || "dev"}\n URL: ${output?.service_url || "pending"}`); - } else { - parts.push(`${status} **Deploy failed** for \`${tool.arguments.service_name}\`: ${run.error?.message}`); - } - break; - - case "get_funnel_analytics": - if (run.status === "succeeded") { - const output = run.output as any; - const steps = output?.steps || []; - const conversion = ((output?.overall_conversion || 0) * 100).toFixed(1); - parts.push(`${status} **Funnel Analysis** (${tool.arguments.range_days || 30} days)\n Overall conversion: ${conversion}%\n Steps: ${steps.length}`); - } - break; - - case "get_top_drivers": - if (run.status === "succeeded") { - const output = run.output as any; - const drivers = output?.drivers || []; - const topDrivers = drivers.slice(0, 3).map((d: any) => d.name).join(", "); - parts.push(`${status} **Top Drivers** for ${tool.arguments.metric}\n ${topDrivers}`); - } - break; - - case "generate_marketing_posts": - if (run.status === "succeeded") { - const output = run.output as any; - const channels = output?.channels || []; - const postCount = channels.reduce((sum: number, c: any) => sum + (c.posts?.length || 0), 0); - parts.push(`${status} **Generated** ${postCount} marketing posts for ${channels.map((c: any) => c.channel).join(", ")}`); - } - break; - - case "get_service_status": - if (run.status === "succeeded") { - const output = run.output as any; - parts.push(`${status} **Service Status**: \`${tool.arguments.service_name}\` is ${output?.status || "unknown"}`); - } - break; - - case "generate_code": - if (run.status === "succeeded") { - parts.push(`${status} **Generated code** for: ${tool.arguments.task}\n File: \`${(run.output as any)?.file_path}\``); - } - break; - - default: - parts.push(`${status} **${tool.name}** completed`); - } - } - - if (parts.length === 0) { - return "I processed your request but no actions were taken."; - } - - return parts.join("\n\n"); -} diff --git a/platform/backend/control-plane/src/routes/health.ts b/platform/backend/control-plane/src/routes/health.ts deleted file mode 100644 index 5b99f1a..0000000 --- a/platform/backend/control-plane/src/routes/health.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { FastifyInstance } from "fastify"; - -export async function healthRoutes(app: FastifyInstance) { - // Root route - API info - app.get("/", async () => ({ - name: "Product OS Control Plane", - version: "0.1.0", - endpoints: { - health: "GET /healthz", - tools: "GET /tools", - invoke: "POST /tools/invoke", - runs: "GET /runs/:run_id" - } - })); - - app.get("/healthz", async () => ({ ok: true })); -} diff --git a/platform/backend/control-plane/src/routes/projects.ts b/platform/backend/control-plane/src/routes/projects.ts deleted file mode 100644 index 8c436f7..0000000 --- a/platform/backend/control-plane/src/routes/projects.ts +++ /dev/null @@ -1,195 +0,0 @@ -import type { FastifyInstance } from "fastify"; -import { nanoid } from "nanoid"; -import { requireAuth } from "../auth.js"; -import { config } from "../config.js"; -import * as gitea from "../gitea.js"; -import * as coolify from "../coolify.js"; -import { - saveProject, - getProject, - listProjects, - updateProjectApp, -} from "../storage/index.js"; -import type { AppRecord, ProjectRecord } from "../types.js"; - -const DEFAULT_APPS: AppRecord[] = [ - { name: "product", path: "apps/product" }, - { name: "website", path: "apps/website" }, - { name: "admin", path: "apps/admin" }, - { name: "storybook", path: "apps/storybook" }, -]; - -const TURBO_VERSION = "2.3.3"; - -interface CreateProjectBody { - name: string; - tenant_id: string; - gitea_owner: string; - apps?: string[]; -} - -interface DeployAppBody { - app_name: string; -} - -function slugify(name: string): string { - return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, ""); -} - -function appDomain(projectSlug: string, appName: string, tenantSlug: string): string { - const base = config.platformUrl.replace(/^https?:\/\//, "").split(":")[0] ?? "vibnai.com"; - return `${appName}-${projectSlug}.${tenantSlug}.${base}`; -} - -export async function projectRoutes(app: FastifyInstance) { - /** - * List all projects for the authenticated tenant - */ - app.get<{ Querystring: { tenant_id: string } }>("/projects", async (req) => { - await requireAuth(req); - const tenantId = (req.query as any).tenant_id as string; - if (!tenantId) return app.httpErrors.badRequest("tenant_id is required"); - return { projects: await listProjects(tenantId) }; - }); - - /** - * Get a single project - */ - app.get<{ Params: { project_id: string } }>("/projects/:project_id", async (req) => { - await requireAuth(req); - const project = await getProject((req.params as any).project_id); - if (!project) return app.httpErrors.notFound("Project not found"); - return project; - }); - - /** - * Create a new project — scaffolds the Turborepo monorepo in Gitea - * and provisions Coolify services for each app. - */ - app.post<{ Body: CreateProjectBody }>("/projects", async (req) => { - await requireAuth(req); - - const { name, tenant_id, gitea_owner, apps: selectedApps } = req.body; - - if (!name || !tenant_id || !gitea_owner) { - return app.httpErrors.badRequest("name, tenant_id, and gitea_owner are required"); - } - - const slug = slugify(name); - const projectId = `proj_${nanoid(12)}`; - const now = new Date().toISOString(); - - const selectedAppNames = selectedApps ?? DEFAULT_APPS.map(a => a.name); - const apps = DEFAULT_APPS.filter(a => selectedAppNames.includes(a.name)); - - const project: ProjectRecord = { - project_id: projectId, - tenant_id, - name, - slug, - status: "provisioning", - repo: "", - apps, - turboVersion: TURBO_VERSION, - created_at: now, - updated_at: now, - }; - - await saveProject(project); - - // Provision asynchronously — return immediately with "provisioning" status - provisionProject(project, gitea_owner).catch(async (err: Error) => { - project.status = "error"; - project.error = err.message; - project.updated_at = new Date().toISOString(); - await saveProject(project); - app.log.error({ projectId, err: err.message }, "Project provisioning failed"); - }); - - return { project_id: projectId, status: "provisioning", slug }; - }); - - /** - * List apps within a project - */ - app.get<{ Params: { project_id: string } }>("/projects/:project_id/apps", async (req) => { - await requireAuth(req); - const project = await getProject((req.params as any).project_id); - if (!project) return app.httpErrors.notFound("Project not found"); - return { apps: project.apps }; - }); - - /** - * Deploy a specific app within the project - */ - app.post<{ Params: { project_id: string }; Body: DeployAppBody }>( - "/projects/:project_id/deploy", - async (req) => { - await requireAuth(req); - const project = await getProject((req.params as any).project_id); - if (!project) return app.httpErrors.notFound("Project not found"); - - const { app_name } = req.body; - const targetApp = project.apps.find(a => a.name === app_name); - if (!targetApp) return app.httpErrors.notFound(`App "${app_name}" not found in project`); - if (!targetApp.coolifyServiceUuid) { - return app.httpErrors.badRequest(`App "${app_name}" has no Coolify service yet`); - } - - const deploymentUuid = await coolify.triggerDeploy(targetApp.coolifyServiceUuid); - return { deployment_uuid: deploymentUuid, app: app_name, status: "deploying" }; - } - ); -} - -/** - * Full provisioning flow — runs after the route returns - */ -async function provisionProject(project: ProjectRecord, giteaOwner: string): Promise { - const repoName = project.slug; - - // 1. Create Gitea repo - const repoUrl = await gitea.createRepo(giteaOwner, repoName, `${project.name} monorepo`); - project.repo = repoUrl; - project.updated_at = new Date().toISOString(); - await saveProject(project); - - // 2. Push Turborepo scaffold - await gitea.scaffoldRepo(giteaOwner, repoName, project.slug, project.name); - - // 3. Register webhook - const webhookUrl = `${config.platformUrl}/webhooks/gitea`; - await gitea.registerWebhook(giteaOwner, repoName, webhookUrl); - - // 4. Create Coolify project - const coolifyProjectUuid = await coolify.createProject( - project.name, - `Coolify project for ${project.name}` - ); - project.coolifyProjectUuid = coolifyProjectUuid; - project.updated_at = new Date().toISOString(); - await saveProject(project); - - // 5. Create a Coolify service per app - for (const projectApp of project.apps) { - const domain = appDomain(project.slug, projectApp.name, project.tenant_id); - const serviceUuid = await coolify.createAppService({ - coolifyProjectUuid, - appName: projectApp.name, - repoUrl, - domain, - }); - - const updatedApp: AppRecord = { - ...projectApp, - coolifyServiceUuid: serviceUuid, - domain, - }; - - await updateProjectApp(project.project_id, updatedApp); - } - - project.status = "active"; - project.updated_at = new Date().toISOString(); - await saveProject(project); -} diff --git a/platform/backend/control-plane/src/routes/runs.ts b/platform/backend/control-plane/src/routes/runs.ts deleted file mode 100644 index 02bded5..0000000 --- a/platform/backend/control-plane/src/routes/runs.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { FastifyInstance } from "fastify"; -import { requireAuth } from "../auth.js"; -import { getRun } from "../storage/index.js"; - -export async function runRoutes(app: FastifyInstance) { - app.get("/runs/:run_id", async (req) => { - await requireAuth(req); - const runId = (req.params as any).run_id as string; - const run = await getRun(runId); - if (!run) return app.httpErrors.notFound("Run not found"); - return run; - }); - - app.get("/runs/:run_id/logs", async (req) => { - await requireAuth(req); - return { note: "V1: logs are in GCS artifacts under runs//" }; - }); -} diff --git a/platform/backend/control-plane/src/routes/tools.ts b/platform/backend/control-plane/src/routes/tools.ts deleted file mode 100644 index 80789fd..0000000 --- a/platform/backend/control-plane/src/routes/tools.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { FastifyInstance } from "fastify"; -import { nanoid } from "nanoid"; -import { requireAuth } from "../auth.js"; -import { getRegistry } from "../registry.js"; -import { saveRun, writeArtifactText } from "../storage/index.js"; -import type { RunRecord, ToolInvokeRequest } from "../types.js"; - -async function postJson(url: string, body: unknown) { - const res = await fetch(url, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(body) - }); - if (!res.ok) { - const txt = await res.text(); - throw new Error(`Executor error ${res.status}: ${txt}`); - } - return res.json() as Promise; -} - -export async function toolRoutes(app: FastifyInstance) { - app.get("/tools", async (req) => { - await requireAuth(req); - const registry = await getRegistry(); - return { tools: Object.values(registry) }; - }); - - app.post<{ Body: ToolInvokeRequest }>("/tools/invoke", async (req) => { - await requireAuth(req); - - const body = req.body; - const registry = await getRegistry(); - const tool = registry[body.tool]; - if (!tool) return app.httpErrors.notFound(`Unknown tool: ${body.tool}`); - - const runId = `run_${new Date().toISOString().replace(/[-:.TZ]/g, "")}_${nanoid(8)}`; - const now = new Date().toISOString(); - - const run: RunRecord = { - run_id: runId, - tenant_id: body.tenant_id, - tool: body.tool, - status: "queued", - created_at: now, - updated_at: now, - input: body.input, - artifacts: { bucket: process.env.GCS_BUCKET_ARTIFACTS ?? "", prefix: `runs/${runId}` } - }; - - await saveRun(run); - await writeArtifactText(`runs/${runId}`, "input.json", JSON.stringify(body, null, 2)); - - try { - run.status = "running"; - run.updated_at = new Date().toISOString(); - await saveRun(run); - - if (body.dry_run) { - run.status = "succeeded"; - run.output = { dry_run: true }; - run.updated_at = new Date().toISOString(); - await saveRun(run); - await writeArtifactText(`runs/${runId}`, "output.json", JSON.stringify(run.output, null, 2)); - return { run_id: runId, status: run.status }; - } - - const execUrl = `${tool.executor.url}${tool.executor.path}`; - const output = await postJson(execUrl, { - run_id: runId, - tenant_id: body.tenant_id, - workspace_id: body.workspace_id, - input: body.input - }); - - run.status = "succeeded"; - run.output = output; - run.updated_at = new Date().toISOString(); - await saveRun(run); - await writeArtifactText(`runs/${runId}`, "output.json", JSON.stringify(output, null, 2)); - - return { run_id: runId, status: run.status }; - } catch (e: any) { - run.status = "failed"; - run.error = { message: e?.message ?? "Unknown error" }; - run.updated_at = new Date().toISOString(); - await saveRun(run); - await writeArtifactText(`runs/${runId}`, "error.json", JSON.stringify(run.error, null, 2)); - return { run_id: runId, status: run.status }; - } - }); -} diff --git a/platform/backend/control-plane/src/storage/firestore.ts b/platform/backend/control-plane/src/storage/firestore.ts deleted file mode 100644 index 38b7133..0000000 --- a/platform/backend/control-plane/src/storage/firestore.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Firestore } from "@google-cloud/firestore"; -import { config } from "../config.js"; -import type { RunRecord, ToolDef } from "../types.js"; - -const db = new Firestore({ projectId: config.projectId }); - -export async function saveRun(run: RunRecord): Promise { - await db.collection(config.runsCollection).doc(run.run_id).set(run, { merge: true }); -} - -export async function getRun(runId: string): Promise { - const snap = await db.collection(config.runsCollection).doc(runId).get(); - return snap.exists ? (snap.data() as RunRecord) : null; -} - -export async function saveTool(tool: ToolDef): Promise { - await db.collection(config.toolsCollection).doc(tool.name).set(tool, { merge: true }); -} - -export async function listTools(): Promise { - const snap = await db.collection(config.toolsCollection).get(); - return snap.docs.map(d => d.data() as ToolDef); -} diff --git a/platform/backend/control-plane/src/storage/gcs.ts b/platform/backend/control-plane/src/storage/gcs.ts deleted file mode 100644 index 1af6d64..0000000 --- a/platform/backend/control-plane/src/storage/gcs.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Storage } from "@google-cloud/storage"; -import { config } from "../config.js"; - -const storage = new Storage({ projectId: config.projectId }); - -export async function writeArtifactText(prefix: string, filename: string, content: string) { - const bucket = storage.bucket(config.artifactsBucket); - const file = bucket.file(`${prefix}/${filename}`); - await file.save(content, { contentType: "text/plain" }); - return { bucket: config.artifactsBucket, path: `${prefix}/${filename}` }; -} diff --git a/platform/backend/control-plane/src/storage/index.ts b/platform/backend/control-plane/src/storage/index.ts deleted file mode 100644 index 18a6c6a..0000000 --- a/platform/backend/control-plane/src/storage/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Storage adapter that switches between GCP (Firestore/GCS) and in-memory - */ -import { config } from "../config.js"; -import * as memory from "./memory.js"; -import * as firestore from "./firestore.js"; -import * as gcs from "./gcs.js"; - -const useMemory = config.storageMode === "memory"; - -if (useMemory) { - console.log("💾 Using in-memory storage (set GCP_PROJECT_ID for Firestore/GCS)"); - memory.seedTools(); -} else { - console.log(`☁️ Using GCP storage (project: ${config.projectId})`); -} - -// Runs -export const saveRun = useMemory ? memory.saveRun : firestore.saveRun; -export const getRun = useMemory ? memory.getRun : firestore.getRun; - -// Tools -export const saveTool = useMemory ? memory.saveTool : firestore.saveTool; -export const listTools = useMemory ? memory.listTools : firestore.listTools; - -// Artifacts -export const writeArtifactText = useMemory ? memory.writeArtifactText : gcs.writeArtifactText; - -// Projects (memory-only until Firestore adapter is extended) -export const saveProject = memory.saveProject; -export const getProject = memory.getProject; -export const listProjects = memory.listProjects; -export const updateProjectApp = memory.updateProjectApp; diff --git a/platform/backend/control-plane/src/storage/memory.ts b/platform/backend/control-plane/src/storage/memory.ts deleted file mode 100644 index 17add33..0000000 --- a/platform/backend/control-plane/src/storage/memory.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * In-memory storage for local development without Firestore/GCS - */ -import type { AppRecord, ProjectRecord, RunRecord, ToolDef } from "../types.js"; - -// In-memory stores -const runs = new Map(); -const tools = new Map(); -const artifacts = new Map(); -const projects = new Map(); - -// Run operations -export async function saveRun(run: RunRecord): Promise { - runs.set(run.run_id, { ...run }); -} - -export async function getRun(runId: string): Promise { - return runs.get(runId) ?? null; -} - -// Tool operations -export async function saveTool(tool: ToolDef): Promise { - tools.set(tool.name, { ...tool }); -} - -export async function listTools(): Promise { - return Array.from(tools.values()); -} - -// Artifact operations -export async function writeArtifactText(prefix: string, filename: string, content: string) { - const path = `${prefix}/${filename}`; - artifacts.set(path, content); - return { bucket: "memory", path }; -} - -// Project operations -export async function saveProject(project: ProjectRecord): Promise { - projects.set(project.project_id, { ...project }); -} - -export async function getProject(projectId: string): Promise { - return projects.get(projectId) ?? null; -} - -export async function listProjects(tenantId: string): Promise { - return Array.from(projects.values()).filter(p => p.tenant_id === tenantId); -} - -export async function updateProjectApp(projectId: string, app: AppRecord): Promise { - const project = projects.get(projectId); - if (!project) throw new Error(`Project not found: ${projectId}`); - const idx = project.apps.findIndex(a => a.name === app.name); - if (idx >= 0) { - project.apps[idx] = app; - } else { - project.apps.push(app); - } - project.updated_at = new Date().toISOString(); - projects.set(projectId, project); -} - -// Seed some example tools for testing -export function seedTools() { - const sampleTools: ToolDef[] = [ - { - name: "cloudrun.deploy_service", - description: "Build and deploy a Cloud Run service", - risk: "medium", - executor: { kind: "http", url: "http://localhost:8090", path: "/execute/cloudrun/deploy" }, - inputSchema: { - type: "object", - required: ["service_name", "repo", "ref", "env"], - properties: { - service_name: { type: "string" }, - repo: { type: "string" }, - ref: { type: "string" }, - env: { type: "string", enum: ["dev", "staging", "prod"] } - } - } - }, - { - name: "cloudrun.get_service_status", - description: "Get Cloud Run service status", - risk: "low", - executor: { kind: "http", url: "http://localhost:8090", path: "/execute/cloudrun/status" }, - inputSchema: { - type: "object", - required: ["service_name", "region"], - properties: { - service_name: { type: "string" }, - region: { type: "string" } - } - } - }, - { - name: "analytics.funnel_summary", - description: "Get funnel metrics for a time window", - risk: "low", - executor: { kind: "http", url: "http://localhost:8091", path: "/execute/analytics/funnel" }, - inputSchema: { - type: "object", - required: ["range_days"], - properties: { - range_days: { type: "integer", minimum: 1, maximum: 365 } - } - } - }, - { - name: "brand.get_profile", - description: "Get tenant brand profile", - risk: "low", - executor: { kind: "http", url: "http://localhost:8092", path: "/execute/brand/get" }, - inputSchema: { - type: "object", - required: ["profile_id"], - properties: { - profile_id: { type: "string" } - } - } - }, - { - name: "marketing.generate_channel_posts", - description: "Generate social posts from a brief", - risk: "low", - executor: { kind: "http", url: "http://localhost:8093", path: "/execute/marketing/generate" }, - inputSchema: { - type: "object", - required: ["brief", "channels"], - properties: { - brief: { type: "object" }, - channels: { type: "array", items: { type: "string" } } - } - } - } - ]; - - for (const tool of sampleTools) { - tools.set(tool.name, tool); - } - - console.log(`📦 Seeded ${sampleTools.length} tools in memory`); -} diff --git a/platform/backend/control-plane/src/types.ts b/platform/backend/control-plane/src/types.ts deleted file mode 100644 index 3ff40dd..0000000 --- a/platform/backend/control-plane/src/types.ts +++ /dev/null @@ -1,65 +0,0 @@ -// ─── Project ────────────────────────────────────────────────────────────────── - -export type ProjectStatus = "provisioning" | "active" | "error" | "archived"; - -export type AppRecord = { - name: string; - path: string; - coolifyServiceUuid?: string; - domain?: string; -}; - -export type ProjectRecord = { - project_id: string; - tenant_id: string; - name: string; - slug: string; - status: ProjectStatus; - repo: string; - coolifyProjectUuid?: string; - apps: AppRecord[]; - turboVersion: string; - created_at: string; - updated_at: string; - error?: string; -}; - -// ─── Tools ──────────────────────────────────────────────────────────────────── - -export type ToolRisk = "low" | "medium" | "high"; - -export type ToolDef = { - name: string; - description: string; - risk: ToolRisk; - executor: { - kind: "http"; - url: string; - path: string; - }; - inputSchema: unknown; - outputSchema?: unknown; -}; - -export type ToolInvokeRequest = { - tool: string; - tenant_id: string; - workspace_id?: string; - input: unknown; - dry_run?: boolean; -}; - -export type RunStatus = "queued" | "running" | "succeeded" | "failed"; - -export type RunRecord = { - run_id: string; - tenant_id: string; - tool: string; - status: RunStatus; - created_at: string; - updated_at: string; - input: unknown; - output?: unknown; - error?: { message: string; details?: unknown }; - artifacts?: { bucket: string; prefix: string }; -}; diff --git a/platform/backend/control-plane/tsconfig.json b/platform/backend/control-plane/tsconfig.json deleted file mode 100644 index a6d228d..0000000 --- a/platform/backend/control-plane/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "Bundler", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "types": ["node"] - } -} diff --git a/platform/backend/executors/analytics/package.json b/platform/backend/executors/analytics/package.json deleted file mode 100644 index dcef35e..0000000 --- a/platform/backend/executors/analytics/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@productos/analytics-executor", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "dist/index.js", - "scripts": { - "dev": "tsx watch src/index.ts", - "build": "tsc -p tsconfig.json", - "start": "node dist/index.js" - }, - "dependencies": { - "@fastify/cors": "^10.0.0", - "@fastify/sensible": "^6.0.0", - "fastify": "^5.0.0" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "tsx": "^4.19.0", - "typescript": "^5.5.4" - } -} diff --git a/platform/backend/executors/analytics/src/index.ts b/platform/backend/executors/analytics/src/index.ts deleted file mode 100644 index c2c2b21..0000000 --- a/platform/backend/executors/analytics/src/index.ts +++ /dev/null @@ -1,91 +0,0 @@ -import Fastify from "fastify"; -import cors from "@fastify/cors"; -import sensible from "@fastify/sensible"; - -const app = Fastify({ logger: true }); - -await app.register(cors, { origin: true }); -await app.register(sensible); - -app.get("/healthz", async () => ({ ok: true, executor: "analytics" })); - -/** - * Get funnel summary - * In production: queries BigQuery - */ -app.post("/execute/analytics/funnel", async (req) => { - const body = req.body as any; - const { input } = body; - - console.log(`📊 Funnel request:`, input); - - // Mock funnel data - const steps = [ - { event_name: "page_view", users: 10000, conversion_from_prev: 1.0 }, - { event_name: "signup_start", users: 3200, conversion_from_prev: 0.32 }, - { event_name: "signup_complete", users: 2100, conversion_from_prev: 0.66 }, - { event_name: "first_action", users: 1400, conversion_from_prev: 0.67 }, - { event_name: "subscription", users: 420, conversion_from_prev: 0.30 } - ]; - - return { - funnel_name: input.funnel?.name ?? "default_funnel", - range_days: input.range_days, - steps, - overall_conversion: 0.042, - generated_at: new Date().toISOString() - }; -}); - -/** - * Get top drivers for a metric - */ -app.post("/execute/analytics/top_drivers", async (req) => { - const body = req.body as any; - const { input } = body; - - console.log(`🔍 Top drivers request:`, input); - - // Mock driver analysis - const drivers = [ - { name: "completed_onboarding", score: 0.85, direction: "positive", evidence: "Users who complete onboarding convert 3.2x more", confidence: 0.92 }, - { name: "used_feature_x", score: 0.72, direction: "positive", evidence: "Feature X usage correlates with 2.5x retention", confidence: 0.88 }, - { name: "time_to_first_value", score: 0.68, direction: "negative", evidence: "Each additional day reduces conversion by 12%", confidence: 0.85 }, - { name: "invited_team_member", score: 0.61, direction: "positive", evidence: "Team invites increase stickiness by 40%", confidence: 0.79 }, - { name: "mobile_signup", score: 0.45, direction: "negative", evidence: "Mobile signups have 25% lower activation", confidence: 0.71 } - ]; - - return { - target: input.target, - range_days: input.range_days, - drivers, - generated_at: new Date().toISOString() - }; -}); - -/** - * Write an insight - */ -app.post("/execute/analytics/write_insight", async (req) => { - const body = req.body as any; - const { input, run_id } = body; - - console.log(`💡 Write insight:`, input.insight?.title); - - const insightId = `insight_${Date.now().toString(36)}`; - - return { - insight_id: insightId, - stored: { - bigquery: { dataset: "insights", table: "insights_v1" }, - firestore: { collection: "insights", doc_id: insightId }, - gcs: { bucket: "productos-artifacts", prefix: `insights/${insightId}` } - }, - created_at: new Date().toISOString() - }; -}); - -const port = Number(process.env.PORT ?? 8091); -app.listen({ port, host: "0.0.0.0" }).then(() => { - console.log(`📈 Analytics Executor running on http://localhost:${port}`); -}); diff --git a/platform/backend/executors/analytics/tsconfig.json b/platform/backend/executors/analytics/tsconfig.json deleted file mode 100644 index a6d228d..0000000 --- a/platform/backend/executors/analytics/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "Bundler", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "types": ["node"] - } -} diff --git a/platform/backend/executors/deploy/package.json b/platform/backend/executors/deploy/package.json deleted file mode 100644 index 77cf9d1..0000000 --- a/platform/backend/executors/deploy/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@productos/deploy-executor", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "dist/index.js", - "scripts": { - "dev": "tsx watch src/index.ts", - "build": "tsc -p tsconfig.json", - "start": "node dist/index.js" - }, - "dependencies": { - "@fastify/cors": "^10.0.0", - "@fastify/sensible": "^6.0.0", - "fastify": "^5.0.0", - "zod": "^3.23.8" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "tsx": "^4.19.0", - "typescript": "^5.5.4" - } -} diff --git a/platform/backend/executors/deploy/src/index.ts b/platform/backend/executors/deploy/src/index.ts deleted file mode 100644 index 103a613..0000000 --- a/platform/backend/executors/deploy/src/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -import Fastify from "fastify"; -import cors from "@fastify/cors"; -import sensible from "@fastify/sensible"; - -const app = Fastify({ logger: true }); - -await app.register(cors, { origin: true }); -await app.register(sensible); - -// Health check -app.get("/healthz", async () => ({ ok: true, executor: "deploy" })); - -/** - * Deploy an app from a Turborepo monorepo. - * - * Expects input to include: - * - repo_url: git clone URL (the project monorepo) - * - app_name: the app folder name under apps/ (e.g. "product", "website") - * - ref: git branch/tag/sha (default "main") - * - env: target environment ("dev" | "staging" | "prod") - * - * Build command: turbo run build --filter={app_name} - * In production this triggers Coolify via its API; in dev it returns a mock. - */ -app.post("/execute/deploy", async (req) => { - const body = req.body as any; - const { run_id, tenant_id, input } = body; - - const appName = input.app_name ?? input.service_name ?? "product"; - const repoUrl = input.repo_url ?? ""; - const ref = input.ref ?? "main"; - const env = input.env ?? "dev"; - - console.log(`🚀 Monorepo deploy request:`, { run_id, tenant_id, appName, repoUrl, ref, env }); - - await new Promise(r => setTimeout(r, 1500)); - - const mockRevision = `${appName}-${Date.now().toString(36)}`; - const mockUrl = `https://${appName}-${ref}.vibnai.com`; - - console.log(`✅ Deploy complete:`, { appName, revision: mockRevision, url: mockUrl }); - - return { - app_name: appName, - service_url: mockUrl, - revision: mockRevision, - build_command: `turbo run build --filter=${appName}`, - build_id: `build-${Date.now()}`, - deployed_at: new Date().toISOString(), - env, - }; -}); - -// Legacy Cloud Run endpoint — kept for backwards compatibility -app.post("/execute/cloudrun/deploy", async (req) => { - const body = req.body as any; - const { run_id, tenant_id, input } = body; - - console.log(`🚀 Deploy request (legacy):`, { run_id, tenant_id, input }); - await new Promise(r => setTimeout(r, 1500)); - - const mockRevision = `${input.service_name}-${Date.now().toString(36)}`; - const mockUrl = `https://${input.service_name}-abc123.a.run.app`; - - return { - service_url: mockUrl, - revision: mockRevision, - build_id: `build-${Date.now()}`, - deployed_at: new Date().toISOString(), - region: input.region ?? "us-central1", - env: input.env, - }; -}); - -/** - * Get Cloud Run service status - */ -app.post("/execute/cloudrun/status", async (req) => { - const body = req.body as any; - const { input } = body; - - console.log(`📊 Status request:`, input); - - // Mock status response - return { - service_name: input.service_name, - region: input.region, - service_url: `https://${input.service_name}-abc123.a.run.app`, - latest_ready_revision: `${input.service_name}-v1`, - status: "ready", - last_deploy_time: new Date().toISOString(), - traffic: [{ revision: `${input.service_name}-v1`, percent: 100 }] - }; -}); - -/** - * Rollback to a previous revision - */ -app.post("/execute/cloudrun/rollback", async (req) => { - const body = req.body as any; - const { input } = body; - - console.log(`⏪ Rollback request:`, input); - - await new Promise(r => setTimeout(r, 1000)); - - return { - service_name: input.service_name, - rolled_back_to: input.target_revision ?? "previous", - status: "ready", - rolled_back_at: new Date().toISOString() - }; -}); - -const port = Number(process.env.PORT ?? 8090); -app.listen({ port, host: "0.0.0.0" }).then(() => { - console.log(`🔧 Deploy Executor running on http://localhost:${port}`); -}); diff --git a/platform/backend/executors/deploy/tsconfig.json b/platform/backend/executors/deploy/tsconfig.json deleted file mode 100644 index a6d228d..0000000 --- a/platform/backend/executors/deploy/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "Bundler", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "types": ["node"] - } -} diff --git a/platform/backend/executors/marketing/package.json b/platform/backend/executors/marketing/package.json deleted file mode 100644 index a995c7a..0000000 --- a/platform/backend/executors/marketing/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@productos/marketing-executor", - "version": "0.1.0", - "private": true, - "type": "module", - "main": "dist/index.js", - "scripts": { - "dev": "tsx watch src/index.ts", - "build": "tsc -p tsconfig.json", - "start": "node dist/index.js" - }, - "dependencies": { - "@fastify/cors": "^10.0.0", - "@fastify/sensible": "^6.0.0", - "fastify": "^5.0.0" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "tsx": "^4.19.0", - "typescript": "^5.5.4" - } -} diff --git a/platform/backend/executors/marketing/src/index.ts b/platform/backend/executors/marketing/src/index.ts deleted file mode 100644 index 06f8543..0000000 --- a/platform/backend/executors/marketing/src/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -import Fastify from "fastify"; -import cors from "@fastify/cors"; -import sensible from "@fastify/sensible"; - -const app = Fastify({ logger: true }); - -await app.register(cors, { origin: true }); -await app.register(sensible); - -app.get("/healthz", async () => ({ ok: true, executor: "marketing" })); - -/** - * Generate channel posts from a brief - * In production: calls Gemini API - */ -app.post("/execute/marketing/generate", async (req) => { - const body = req.body as any; - const { input } = body; - - console.log(`✍️ Generate posts:`, input.brief?.goal); - - await new Promise(r => setTimeout(r, 1000)); // Simulate AI generation time - - const channels = (input.channels ?? ["x", "linkedin"]).map((channel: string) => ({ - channel, - posts: [ - { - text: `🚀 Exciting news! ${input.brief?.product ?? "Our product"} just got even better. ${input.brief?.key_points?.[0] ?? "Check it out!"}\n\n${input.brief?.call_to_action ?? "Learn more"} 👇\n${input.brief?.landing_page_url ?? "https://example.com"}`, - hashtags: ["#ProductUpdate", "#SaaS", "#Innovation"], - media_suggestions: ["product-screenshot.png", "feature-demo.gif"] - }, - { - text: `${input.brief?.audience ?? "Teams"} asked, we listened! Introducing ${input.brief?.product ?? "new features"} that will transform how you work.\n\n✨ ${input.brief?.key_points?.join("\n✨ ") ?? "Amazing new capabilities"}\n\nTry it today!`, - hashtags: ["#ProductLaunch", "#Productivity"], - media_suggestions: ["comparison-chart.png"] - }, - { - text: `Did you know? ${input.brief?.key_points?.[0] ?? "Our latest update"} can save you hours every week.\n\nHere's how ${input.brief?.product ?? "it"} works:\n1️⃣ Set it up in minutes\n2️⃣ Let automation do the work\n3️⃣ Focus on what matters\n\n${input.brief?.offer ?? "Start free today!"}`, - hashtags: ["#Automation", "#WorkSmarter"], - media_suggestions: ["how-it-works.mp4"] - } - ] - })); - - return { - channels, - brief_summary: input.brief?.goal, - generated_at: new Date().toISOString(), - variations_per_channel: 3 - }; -}); - -/** - * Get brand profile - */ -app.post("/execute/brand/get", async (req) => { - const body = req.body as any; - const { input } = body; - - console.log(`🎨 Get brand profile:`, input.profile_id); - - return { - profile_id: input.profile_id ?? "default", - brand: { - name: "Product OS", - voice: { - tone: ["professional", "friendly", "innovative"], - style_notes: ["Use active voice", "Keep sentences short", "Be specific with benefits"], - do: ["Use data and metrics", "Include calls to action", "Highlight customer success"], - dont: ["Make unverified claims", "Use jargon", "Be overly salesy"] - }, - audience: { - primary: "SaaS founders and product teams", - secondary: "Growth marketers and developers" - }, - claims_policy: { - forbidden_claims: ["#1 in the market", "Guaranteed results"], - required_disclaimers: [], - compliance_notes: ["All metrics should be verifiable"] - } - } - }; -}); - -const port = Number(process.env.PORT ?? 8093); -app.listen({ port, host: "0.0.0.0" }).then(() => { - console.log(`📣 Marketing Executor running on http://localhost:${port}`); -}); diff --git a/platform/backend/executors/marketing/tsconfig.json b/platform/backend/executors/marketing/tsconfig.json deleted file mode 100644 index a6d228d..0000000 --- a/platform/backend/executors/marketing/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "Bundler", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "types": ["node"] - } -} diff --git a/platform/backend/mcp-adapter/README.md b/platform/backend/mcp-adapter/README.md deleted file mode 100644 index cc04912..0000000 --- a/platform/backend/mcp-adapter/README.md +++ /dev/null @@ -1,103 +0,0 @@ -# Product OS MCP Adapter - -Exposes Control Plane tools to Continue and other MCP clients. - -## Architecture - -``` -Continue (MCP client) - ↓ - MCP Adapter (this server) - ↓ - Control Plane API - ↓ - Gemini + Executors -``` - -## Available Tools - -| Tool | Description | -|------|-------------| -| `deploy_service` | Deploy a Cloud Run service | -| `get_service_status` | Check deployment health | -| `get_funnel_analytics` | Get conversion metrics | -| `get_top_drivers` | Understand metric drivers | -| `generate_marketing_posts` | Create social content | -| `chat_with_gemini` | General AI conversation | - -## Setup - -### 1. Install dependencies - -```bash -cd platform/backend/mcp-adapter -npm install -``` - -### 2. Make sure Control Plane is running - -```bash -cd platform/backend/control-plane -npm run dev -``` - -### 3. Install Continue Extension - -In VS Code/VSCodium: -1. Open Extensions (Cmd+Shift+X) -2. Search for "Continue" -3. Install - -### 4. Configure Continue - -The configuration is already in `.continue/config.yaml`. - -Or manually add to your Continue config: - -```yaml -experimental: - modelContextProtocolServers: - - name: productos - command: npx - args: - - tsx - - /path/to/platform/backend/mcp-adapter/src/index.ts - env: - CONTROL_PLANE_URL: http://localhost:8080 -``` - -### 5. Use in Continue - -1. Open Continue chat (Cmd+L) -2. Enable Agent mode (click the robot icon) -3. Ask: "Deploy my service to staging" - -## Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `CONTROL_PLANE_URL` | `http://localhost:8080` | Control Plane API URL | -| `TENANT_ID` | `t_mcp` | Tenant ID for tool calls | - -## Development - -```bash -# Run directly (for testing) -npm run dev - -# Build -npm run build - -# Run built version -npm start -``` - -## Testing - -```bash -# Test tool listing -echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | npm run dev - -# Test tool call -echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_funnel_analytics","arguments":{}}}' | npm run dev -``` diff --git a/platform/backend/mcp-adapter/package.json b/platform/backend/mcp-adapter/package.json deleted file mode 100644 index ed13ef3..0000000 --- a/platform/backend/mcp-adapter/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@productos/mcp-adapter", - "version": "0.1.0", - "private": true, - "description": "MCP Adapter Server - exposes Control Plane tools to Continue and other MCP clients", - "type": "module", - "main": "dist/index.js", - "bin": { - "productos-mcp": "./dist/index.js" - }, - "scripts": { - "dev": "tsx src/index.ts", - "build": "tsc -p tsconfig.json", - "start": "node dist/index.js" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0", - "zod": "^3.23.8" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "tsx": "^4.19.0", - "typescript": "^5.5.4" - } -} diff --git a/platform/backend/mcp-adapter/src/index.ts b/platform/backend/mcp-adapter/src/index.ts deleted file mode 100644 index e88269f..0000000 --- a/platform/backend/mcp-adapter/src/index.ts +++ /dev/null @@ -1,343 +0,0 @@ -#!/usr/bin/env node -/** - * Product OS MCP Adapter Server - * - * Exposes Control Plane tools to Continue and other MCP clients. - * - * Architecture: - * Continue (MCP client) → This Adapter → Control Plane → Gemini + Executors - * - * This keeps: - * - Control Plane as the canonical API - * - Auth/billing/policies centralized - * - MCP as a compatibility layer - */ - -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, - ToolSchema, -} from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; - -// Control Plane URL (configurable via env) -const CONTROL_PLANE_URL = process.env.CONTROL_PLANE_URL || "http://localhost:8080"; -const TENANT_ID = process.env.TENANT_ID || "t_mcp"; - -// ============================================================================ -// Tool Definitions -// ============================================================================ - -const TOOLS: ToolSchema[] = [ - { - name: "deploy_service", - description: "Deploy a Cloud Run service to GCP. Use when the user wants to deploy, ship, or launch code to staging or production.", - inputSchema: { - type: "object", - properties: { - service_name: { - type: "string", - description: "Name of the service to deploy" - }, - repo: { - type: "string", - description: "Git repository URL (optional, defaults to current workspace)" - }, - ref: { - type: "string", - description: "Git branch, tag, or commit (optional, defaults to main)" - }, - env: { - type: "string", - enum: ["dev", "staging", "prod"], - description: "Target environment" - } - }, - required: ["service_name"] - } - }, - { - name: "get_service_status", - description: "Check the status of a deployed Cloud Run service. Use when the user asks about service health or deployment status.", - inputSchema: { - type: "object", - properties: { - service_name: { - type: "string", - description: "Name of the service to check" - }, - region: { - type: "string", - description: "GCP region (defaults to us-central1)" - } - }, - required: ["service_name"] - } - }, - { - name: "get_funnel_analytics", - description: "Get funnel conversion metrics and drop-off analysis. Use when the user asks about funnels, conversions, or user journey.", - inputSchema: { - type: "object", - properties: { - funnel_name: { - type: "string", - description: "Name of the funnel to analyze (optional)" - }, - range_days: { - type: "integer", - description: "Number of days to analyze (defaults to 30)" - } - } - } - }, - { - name: "get_top_drivers", - description: "Identify top factors driving a metric. Use when the user asks why something changed or what drives conversions/retention.", - inputSchema: { - type: "object", - properties: { - metric: { - type: "string", - description: "The metric to analyze (e.g., 'conversion', 'retention', 'churn')" - }, - range_days: { - type: "integer", - description: "Number of days to analyze (defaults to 30)" - } - }, - required: ["metric"] - } - }, - { - name: "generate_marketing_posts", - description: "Generate social media posts for a marketing campaign. Use when the user wants to create content for X, LinkedIn, etc.", - inputSchema: { - type: "object", - properties: { - goal: { - type: "string", - description: "Campaign goal (e.g., 'product launch', 'feature announcement', 'engagement')" - }, - product: { - type: "string", - description: "Product or feature name" - }, - channels: { - type: "array", - items: { type: "string" }, - description: "Social channels to generate for (e.g., ['x', 'linkedin'])" - }, - tone: { - type: "string", - description: "Tone of voice (e.g., 'professional', 'casual', 'excited')" - } - }, - required: ["goal"] - } - }, - { - name: "chat_with_gemini", - description: "Have a conversation with Gemini AI about your product, code, or anything else. Use for general questions, code explanation, or ideation.", - inputSchema: { - type: "object", - properties: { - message: { - type: "string", - description: "The message or question to send to Gemini" - }, - context: { - type: "string", - description: "Additional context (e.g., code snippet, file contents)" - } - }, - required: ["message"] - } - } -]; - -// ============================================================================ -// Tool Name Mapping (MCP tool name → Control Plane tool name) -// ============================================================================ - -const TOOL_MAPPING: Record = { - "deploy_service": "cloudrun.deploy_service", - "get_service_status": "cloudrun.get_service_status", - "get_funnel_analytics": "analytics.funnel_summary", - "get_top_drivers": "analytics.top_drivers", - "generate_marketing_posts": "marketing.generate_channel_posts", -}; - -// ============================================================================ -// Control Plane Client -// ============================================================================ - -async function invokeControlPlaneTool(tool: string, input: any): Promise { - // Step 1: Invoke the tool - const invokeResponse = await fetch(`${CONTROL_PLANE_URL}/tools/invoke`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - tool, - tenant_id: TENANT_ID, - input, - dry_run: false - }) - }); - - if (!invokeResponse.ok) { - const text = await invokeResponse.text(); - throw new Error(`Control Plane error: ${invokeResponse.status} - ${text}`); - } - - const invokeResult = await invokeResponse.json(); - - // Step 2: Fetch the full run to get the output - const runResponse = await fetch(`${CONTROL_PLANE_URL}/runs/${invokeResult.run_id}`); - - if (!runResponse.ok) { - // Fall back to the invoke result if we can't fetch the run - return invokeResult; - } - - return runResponse.json(); -} - -async function chatWithControlPlane(message: string, context?: string): Promise { - const messages = []; - - if (context) { - messages.push({ role: "user", content: `Context:\n${context}` }); - } - messages.push({ role: "user", content: message }); - - const response = await fetch(`${CONTROL_PLANE_URL}/chat`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - messages, - autoExecuteTools: true - }) - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Control Plane chat error: ${response.status} - ${text}`); - } - - return response.json(); -} - -// ============================================================================ -// MCP Server -// ============================================================================ - -const server = new Server( - { - name: "productos-mcp", - version: "0.1.0", - }, - { - capabilities: { - tools: {}, - }, - } -); - -// List available tools -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { tools: TOOLS }; -}); - -// Handle tool calls -server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - console.error(`[MCP] Tool called: ${name}`, args); - - try { - // Special handling for chat - if (name === "chat_with_gemini") { - const result = await chatWithControlPlane( - (args as any).message, - (args as any).context - ); - - return { - content: [ - { - type: "text", - text: result.message || JSON.stringify(result, null, 2) - } - ] - }; - } - - // Map MCP tool name to Control Plane tool name - const controlPlaneTool = TOOL_MAPPING[name]; - if (!controlPlaneTool) { - throw new Error(`Unknown tool: ${name}`); - } - - // Invoke the Control Plane - const run = await invokeControlPlaneTool(controlPlaneTool, args); - - // Format the response - let responseText = ""; - - if (run.status === "succeeded") { - responseText = `✅ **${name}** completed successfully\n\n`; - responseText += "**Result:**\n```json\n"; - responseText += JSON.stringify(run.output, null, 2); - responseText += "\n```"; - } else if (run.status === "failed") { - responseText = `❌ **${name}** failed\n\n`; - responseText += `**Error:** ${run.error?.message || "Unknown error"}`; - } else { - responseText = `⏳ **${name}** is ${run.status}\n\n`; - responseText += `**Run ID:** ${run.run_id}`; - } - - return { - content: [ - { - type: "text", - text: responseText - } - ] - }; - - } catch (error: any) { - console.error(`[MCP] Error:`, error); - return { - content: [ - { - type: "text", - text: `❌ Error executing ${name}: ${error.message}` - } - ], - isError: true - }; - } -}); - -// ============================================================================ -// Start Server -// ============================================================================ - -async function main() { - console.error("[MCP] Product OS MCP Adapter starting..."); - console.error(`[MCP] Control Plane URL: ${CONTROL_PLANE_URL}`); - - const transport = new StdioServerTransport(); - await server.connect(transport); - - console.error("[MCP] Server connected and ready"); -} - -main().catch((error) => { - console.error("[MCP] Fatal error:", error); - process.exit(1); -}); diff --git a/platform/backend/mcp-adapter/tsconfig.json b/platform/backend/mcp-adapter/tsconfig.json deleted file mode 100644 index 486fed8..0000000 --- a/platform/backend/mcp-adapter/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "declaration": true - }, - "include": ["src/**/*"] -} diff --git a/platform/client-ide/extensions/gcp-productos/media/icon.svg b/platform/client-ide/extensions/gcp-productos/media/icon.svg deleted file mode 100644 index 47547c8..0000000 --- a/platform/client-ide/extensions/gcp-productos/media/icon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/platform/client-ide/extensions/gcp-productos/package.json b/platform/client-ide/extensions/gcp-productos/package.json deleted file mode 100644 index cbd37cc..0000000 --- a/platform/client-ide/extensions/gcp-productos/package.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "name": "gcp-productos", - "displayName": "GCP Product OS", - "description": "Product-centric IDE for launching and operating SaaS products on Google Cloud. Use @productos in chat!", - "version": "0.2.0", - "publisher": "productos", - "engines": { "vscode": "^1.90.0" }, - "categories": ["AI", "Chat", "Other"], - "activationEvents": ["onStartupFinished"], - "main": "./dist/extension.js", - "contributes": { - "chatParticipants": [ - { - "id": "productos.chat", - "fullName": "Product OS", - "name": "productos", - "description": "Deploy, analyze, and automate your SaaS product on Google Cloud", - "isSticky": true, - "commands": [ - { - "name": "deploy", - "description": "Deploy a service to Cloud Run" - }, - { - "name": "analytics", - "description": "Get funnel and conversion analytics" - }, - { - "name": "marketing", - "description": "Generate marketing content" - }, - { - "name": "status", - "description": "Check service status" - } - ] - } - ], - "viewsContainers": { - "activitybar": [ - { - "id": "productos", - "title": "Product OS", - "icon": "media/icon.svg" - } - ] - }, - "views": { - "productos": [ - { - "id": "productos.tools", - "name": "Tools", - "icon": "media/icon.svg" - }, - { - "id": "productos.runs", - "name": "Recent Runs" - } - ] - }, - "viewsWelcome": [ - { - "view": "productos.tools", - "contents": "Connect to Product OS backend to see available tools.\n[Configure Backend](command:productos.configure)" - } - ], - "commands": [ - { "command": "productos.configure", "title": "Product OS: Configure Backend", "icon": "$(gear)" }, - { "command": "productos.refresh", "title": "Product OS: Refresh", "icon": "$(refresh)" }, - { "command": "productos.tools.list", "title": "Product OS: List Tools" }, - { "command": "productos.tools.invoke", "title": "Product OS: Invoke Tool", "icon": "$(play)" }, - { "command": "productos.tools.invokeFromTree", "title": "Invoke Tool", "icon": "$(play)" }, - { "command": "productos.runs.open", "title": "Product OS: Open Run" }, - { "command": "productos.runs.openFromTree", "title": "View Run Details", "icon": "$(eye)" } - ], - "menus": { - "view/title": [ - { "command": "productos.refresh", "when": "view == productos.tools", "group": "navigation" }, - { "command": "productos.configure", "when": "view == productos.tools" } - ], - "view/item/context": [ - { "command": "productos.tools.invokeFromTree", "when": "viewItem == tool", "group": "inline" }, - { "command": "productos.runs.openFromTree", "when": "viewItem == run", "group": "inline" } - ] - }, - "configuration": { - "title": "Product OS", - "properties": { - "productos.backendUrl": { - "type": "string", - "default": "http://localhost:8080", - "description": "Control Plane API base URL" - }, - "productos.tenantId": { - "type": "string", - "default": "t_dev", - "description": "Tenant ID for tool calls" - } - } - } - }, - "scripts": { - "build": "tsc -p tsconfig.json", - "watch": "tsc -w -p tsconfig.json", - "package": "vsce package --allow-missing-repository" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "@types/vscode": "^1.90.0", - "@vscode/vsce": "^3.0.0", - "typescript": "^5.5.4" - } -} diff --git a/platform/client-ide/extensions/gcp-productos/src/api.ts b/platform/client-ide/extensions/gcp-productos/src/api.ts deleted file mode 100644 index ea5cc9d..0000000 --- a/platform/client-ide/extensions/gcp-productos/src/api.ts +++ /dev/null @@ -1,137 +0,0 @@ -import * as vscode from "vscode"; - -export interface Tool { - name: string; - description: string; - risk: "low" | "medium" | "high"; - executor: { - kind: string; - url: string; - path: string; - }; - inputSchema: any; - outputSchema?: any; -} - -export interface Run { - run_id: string; - tenant_id: string; - tool: string; - status: "queued" | "running" | "succeeded" | "failed"; - created_at: string; - updated_at: string; - input: any; - output?: any; - error?: { message: string; details?: any }; -} - -function getConfig(key: string): T { - return vscode.workspace.getConfiguration("productos").get(key)!; -} - -export function getBackendUrl(): string { - return getConfig("backendUrl"); -} - -export function getTenantId(): string { - return getConfig("tenantId"); -} - -export async function checkConnection(): Promise { - try { - const res = await fetch(`${getBackendUrl()}/healthz`, { - signal: AbortSignal.timeout(3000) - }); - return res.ok; - } catch { - return false; - } -} - -export async function listTools(): Promise { - const res = await fetch(`${getBackendUrl()}/tools`); - if (!res.ok) throw new Error(await res.text()); - const json = await res.json(); - return json.tools ?? []; -} - -export async function invokeTool(tool: string, input: any, dryRun = false): Promise { - const res = await fetch(`${getBackendUrl()}/tools/invoke`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - tool, - tenant_id: getTenantId(), - input, - dry_run: dryRun - }) - }); - if (!res.ok) throw new Error(await res.text()); - return res.json(); -} - -export async function getRun(runId: string): Promise { - const res = await fetch(`${getBackendUrl()}/runs/${runId}`); - if (!res.ok) throw new Error(await res.text()); - return res.json(); -} - -// Store recent runs in memory -const recentRuns: Run[] = []; - -export function addRecentRun(run: Run) { - recentRuns.unshift(run); - if (recentRuns.length > 20) recentRuns.pop(); -} - -export function getRecentRuns(): Run[] { - return [...recentRuns]; -} - -// Chat types -export interface ChatMessage { - role: "user" | "assistant" | "system"; - content: string; -} - -export interface ToolCall { - name: string; - arguments: Record; -} - -export interface ChatResponse { - message: string; - toolCalls?: ToolCall[]; - runs?: Run[]; - finishReason: "stop" | "tool_calls" | "error"; -} - -export interface ChatContext { - files?: { path: string; content: string }[]; - selection?: { path: string; text: string; startLine: number }; -} - -/** - * Chat with the AI backend - */ -export async function chatWithAI( - messages: ChatMessage[], - context?: ChatContext -): Promise { - const res = await fetch(`${getBackendUrl()}/chat`, { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - messages, - context, - autoExecuteTools: true - }) - }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(`Chat failed: ${text}`); - } - - return res.json(); -} diff --git a/platform/client-ide/extensions/gcp-productos/src/chatPanel.ts b/platform/client-ide/extensions/gcp-productos/src/chatPanel.ts deleted file mode 100644 index df6757d..0000000 --- a/platform/client-ide/extensions/gcp-productos/src/chatPanel.ts +++ /dev/null @@ -1,850 +0,0 @@ -import * as vscode from "vscode"; -import { chatWithAI, ChatMessage, ChatResponse } from "./api"; - -/** - * Product OS Chat Panel - * A Cursor-like conversational AI interface - */ -export class ChatPanel { - public static currentPanel: ChatPanel | undefined; - private static readonly viewType = "productosChat"; - - private readonly _panel: vscode.WebviewPanel; - private readonly _extensionUri: vscode.Uri; - private _disposables: vscode.Disposable[] = []; - private _messages: ChatMessage[] = []; - - public static createOrShow(extensionUri: vscode.Uri) { - const column = vscode.window.activeTextEditor - ? vscode.window.activeTextEditor.viewColumn - : undefined; - - // If we already have a panel, show it - if (ChatPanel.currentPanel) { - ChatPanel.currentPanel._panel.reveal(column); - return; - } - - // Otherwise, create a new panel - const panel = vscode.window.createWebviewPanel( - ChatPanel.viewType, - "Product OS Chat", - column || vscode.ViewColumn.One, - { - enableScripts: true, - retainContextWhenHidden: true, - localResourceRoots: [vscode.Uri.joinPath(extensionUri, "media")] - } - ); - - ChatPanel.currentPanel = new ChatPanel(panel, extensionUri); - } - - private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) { - this._panel = panel; - this._extensionUri = extensionUri; - - // Set the webview's initial html content - this._update(); - - // Listen for when the panel is disposed - this._panel.onDidDispose(() => this.dispose(), null, this._disposables); - - // Handle messages from the webview - this._panel.webview.onDidReceiveMessage( - async (message) => { - switch (message.command) { - case "send": - await this._handleChat(message.text); - return; - case "addContext": - await this._handleAddContext(); - return; - case "clear": - this._messages = []; - this._update(); - return; - } - }, - null, - this._disposables - ); - } - - private async _handleChat(text: string) { - // Add user message to history (webview already shows it) - this._messages.push({ role: "user", content: text }); - // DON'T call _update() - it would reset the webview and kill the JS state - - // Show loading state - this._panel.webview.postMessage({ type: "loading", loading: true }); - - try { - // Get context from active editor - const context = this._getEditorContext(); - - console.log("[Product OS Chat] Sending to API:", text); - - // Call the AI - const response = await chatWithAI(this._messages, context); - - console.log("[Product OS Chat] Response:", response); - - // Add assistant response to history - this._messages.push({ role: "assistant", content: response.message || "" }); - - // Send response to webview - this._panel.webview.postMessage({ - type: "response", - message: response.message, - toolCalls: response.toolCalls, - runs: response.runs - }); - - } catch (error: any) { - console.error("[Product OS Chat] Error:", error); - this._panel.webview.postMessage({ - type: "error", - error: error.message || "Unknown error" - }); - } finally { - this._panel.webview.postMessage({ type: "loading", loading: false }); - } - } - - private async _handleAddContext() { - const editor = vscode.window.activeTextEditor; - if (!editor) { - vscode.window.showWarningMessage("No active editor"); - return; - } - - const selection = editor.selection; - const selectedText = editor.document.getText(selection); - - if (selectedText) { - const filePath = vscode.workspace.asRelativePath(editor.document.uri); - const startLine = selection.start.line + 1; - - this._panel.webview.postMessage({ - type: "contextAdded", - context: { - type: "selection", - path: filePath, - startLine, - text: selectedText - } - }); - } else { - // No selection, add the whole file - const filePath = vscode.workspace.asRelativePath(editor.document.uri); - const content = editor.document.getText(); - - this._panel.webview.postMessage({ - type: "contextAdded", - context: { - type: "file", - path: filePath, - text: content.substring(0, 5000) // Limit to first 5000 chars - } - }); - } - } - - private _getEditorContext(): any { - const editor = vscode.window.activeTextEditor; - if (!editor) return undefined; - - const selection = editor.selection; - const selectedText = editor.document.getText(selection); - - if (selectedText) { - return { - selection: { - path: vscode.workspace.asRelativePath(editor.document.uri), - text: selectedText, - startLine: selection.start.line + 1 - } - }; - } - - return undefined; - } - - public dispose() { - ChatPanel.currentPanel = undefined; - - // Clean up resources - this._panel.dispose(); - - while (this._disposables.length) { - const x = this._disposables.pop(); - if (x) { - x.dispose(); - } - } - } - - private _update() { - this._panel.webview.html = this._getHtmlForWebview(); - } - - private _getHtmlForWebview() { - const nonce = getNonce(); - - // Convert messages to HTML - const messagesHtml = this._messages.map(m => { - const isUser = m.role === "user"; - const avatarClass = isUser ? "user-avatar" : "ai-avatar"; - const messageClass = isUser ? "user-message" : "ai-message"; - const avatar = isUser ? "U" : "✦"; - - return ` -
-
${avatar}
-
${escapeHtml(m.content)}
-
- `; - }).join(""); - - return ` - - - - - - Product OS Chat - - - -
-
-
-

Product OS Chat

-
-
- -
-
- -
- ${messagesHtml || ` -
-
-

Welcome to Product OS

-

I can help you deploy services, analyze metrics, generate marketing content, and write code—all in one place.

-
- - - - -
-
- `} -
- -
-
-
-
-
-
- Product OS is thinking... -
- -
-
- 📎 Context: - - -
-
-
- - -
- -
-
- - - -`; - } -} - -function getNonce() { - let text = ""; - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} - -function escapeHtml(text: string): string { - return text - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'") - .replace(/\n/g, "
"); -} diff --git a/platform/client-ide/extensions/gcp-productos/src/chatParticipant.ts b/platform/client-ide/extensions/gcp-productos/src/chatParticipant.ts deleted file mode 100644 index 838976e..0000000 --- a/platform/client-ide/extensions/gcp-productos/src/chatParticipant.ts +++ /dev/null @@ -1,223 +0,0 @@ -import * as vscode from "vscode"; -import { getBackendUrl } from "./api"; - -/** - * Product OS Chat Participant - * - * Registers @productos in the native VS Code chat panel. - * Users can type "@productos deploy to staging" and get responses - * in the same UI as GitHub Copilot. - */ - -// Chat response interface from Control Plane -interface ChatResponse { - message: string; - toolCalls?: { name: string; arguments: any }[]; - runs?: any[]; - finishReason: string; -} - -/** - * Register the Product OS chat participant - */ -export function registerChatParticipant(context: vscode.ExtensionContext) { - // Create the chat participant - const participant = vscode.chat.createChatParticipant( - "productos.chat", - chatHandler - ); - - // Set participant properties - participant.iconPath = vscode.Uri.joinPath(context.extensionUri, "media", "icon.svg"); - - // Add follow-up provider for suggestions - participant.followupProvider = { - provideFollowups( - result: vscode.ChatResult, - context: vscode.ChatContext, - token: vscode.CancellationToken - ): vscode.ProviderResult { - // Suggest follow-up actions based on what was just done - return [ - { prompt: "Show me funnel analytics", label: "📊 Analytics" }, - { prompt: "Deploy to staging", label: "🚀 Deploy" }, - { prompt: "Generate marketing posts", label: "📣 Marketing" }, - { prompt: "What drives conversions?", label: "📈 Drivers" } - ]; - } - }; - - context.subscriptions.push(participant); - - console.log("[Product OS] Chat participant @productos registered"); -} - -/** - * Handle chat requests - */ -async function chatHandler( - request: vscode.ChatRequest, - context: vscode.ChatContext, - response: vscode.ChatResponseStream, - token: vscode.CancellationToken -): Promise { - - const userPrompt = request.prompt; - console.log("[Product OS] Chat request:", userPrompt); - - // Show progress - response.progress("Connecting to Product OS..."); - - try { - // Build message history from context - const messages = buildMessageHistory(context, userPrompt); - - // Get code context if available - const codeContext = await getCodeContext(); - - // Call the Control Plane - const result = await callControlPlane(messages, codeContext); - - // Handle tool calls - if (result.toolCalls && result.toolCalls.length > 0) { - for (const toolCall of result.toolCalls) { - response.markdown(`\n\n**🔧 Executing:** \`${toolCall.name}\`\n`); - } - } - - // Handle runs (tool execution results) - if (result.runs && result.runs.length > 0) { - for (const run of result.runs) { - const status = run.status === "succeeded" ? "✅" : "❌"; - response.markdown(`\n${status} **${run.tool}**\n`); - - if (run.output) { - response.markdown("\n```json\n" + JSON.stringify(run.output, null, 2) + "\n```\n"); - } - - if (run.error) { - response.markdown(`\n**Error:** ${run.error.message}\n`); - } - } - } - - // Stream the main response - if (result.message) { - response.markdown(result.message); - } - - return { metadata: { command: "chat" } }; - - } catch (error: any) { - console.error("[Product OS] Chat error:", error); - - response.markdown(`\n\n❌ **Error:** ${error.message}\n\n`); - response.markdown("Make sure the Control Plane is running at " + getBackendUrl()); - - return { - metadata: { command: "error" }, - errorDetails: { message: error.message } - }; - } -} - -/** - * Build message history from chat context - */ -function buildMessageHistory( - context: vscode.ChatContext, - currentPrompt: string -): { role: string; content: string }[] { - const messages: { role: string; content: string }[] = []; - - // Add previous messages from context - for (const turn of context.history) { - if (turn instanceof vscode.ChatRequestTurn) { - messages.push({ role: "user", content: turn.prompt }); - } else if (turn instanceof vscode.ChatResponseTurn) { - // Extract text from response - let text = ""; - for (const part of turn.response) { - if (part instanceof vscode.ChatResponseMarkdownPart) { - text += part.value.value; - } - } - if (text) { - messages.push({ role: "assistant", content: text }); - } - } - } - - // Add current prompt - messages.push({ role: "user", content: currentPrompt }); - - return messages; -} - -/** - * Get context from the active editor - */ -async function getCodeContext(): Promise { - const editor = vscode.window.activeTextEditor; - if (!editor) return undefined; - - const selection = editor.selection; - const selectedText = editor.document.getText(selection); - - if (selectedText) { - return { - selection: { - path: vscode.workspace.asRelativePath(editor.document.uri), - text: selectedText, - startLine: selection.start.line + 1 - } - }; - } - - // No selection - include some context from the current file - const document = editor.document; - const cursorLine = selection.active.line; - const startLine = Math.max(0, cursorLine - 20); - const endLine = Math.min(document.lineCount - 1, cursorLine + 20); - - const range = new vscode.Range(startLine, 0, endLine, document.lineAt(endLine).text.length); - const surroundingCode = document.getText(range); - - if (surroundingCode.trim()) { - return { - files: [{ - path: vscode.workspace.asRelativePath(editor.document.uri), - content: surroundingCode - }] - }; - } - - return undefined; -} - -/** - * Call the Control Plane chat endpoint - */ -async function callControlPlane( - messages: { role: string; content: string }[], - context?: any -): Promise { - const backendUrl = getBackendUrl(); - - const response = await fetch(`${backendUrl}/chat`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - messages, - context, - autoExecuteTools: true - }) - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Control Plane error: ${response.status} - ${text}`); - } - - return response.json(); -} diff --git a/platform/client-ide/extensions/gcp-productos/src/chatViewProvider.ts b/platform/client-ide/extensions/gcp-productos/src/chatViewProvider.ts deleted file mode 100644 index 40676bb..0000000 --- a/platform/client-ide/extensions/gcp-productos/src/chatViewProvider.ts +++ /dev/null @@ -1,688 +0,0 @@ -import * as vscode from "vscode"; -import { chatWithAI, ChatMessage, ChatResponse } from "./api"; - -/** - * Sidebar Chat View Provider - * Embedded chat experience in the Product OS sidebar - */ -export class ChatViewProvider implements vscode.WebviewViewProvider { - public static readonly viewType = "productos.chat"; - - private _view?: vscode.WebviewView; - private _messages: ChatMessage[] = []; - - constructor(private readonly _extensionUri: vscode.Uri) {} - - public resolveWebviewView( - webviewView: vscode.WebviewView, - _context: vscode.WebviewViewResolveContext, - _token: vscode.CancellationToken - ) { - this._view = webviewView; - - webviewView.webview.options = { - enableScripts: true, - localResourceRoots: [this._extensionUri] - }; - - webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); - - webviewView.webview.onDidReceiveMessage(async (message) => { - switch (message.command) { - case "send": - await this._handleChat(message.text); - return; - case "addContext": - await this._handleAddContext(); - return; - case "clear": - this._messages = []; - this._updateView(); - return; - } - }); - } - - private async _handleChat(text: string) { - if (!this._view) return; - - // Add user message to internal history (webview already shows it) - this._messages.push({ role: "user", content: text }); - // DON'T call _updateView() - it would reset the webview and kill the JS state - - // Show loading - this._view.webview.postMessage({ type: "loading", loading: true }); - - try { - // Get editor context - const context = this._getEditorContext(); - - console.log("[Product OS Chat] Sending to API:", text); - - // Call AI - const response = await chatWithAI(this._messages, context); - - console.log("[Product OS Chat] Response:", response); - - // Add assistant response to history - this._messages.push({ role: "assistant", content: response.message || "" }); - - // Send to webview - this._view.webview.postMessage({ - type: "response", - message: response.message, - toolCalls: response.toolCalls, - runs: response.runs - }); - } catch (error: any) { - console.error("[Product OS Chat] Error:", error); - this._view.webview.postMessage({ - type: "error", - error: error.message || "Unknown error" - }); - } finally { - this._view.webview.postMessage({ type: "loading", loading: false }); - } - } - - private async _handleAddContext() { - if (!this._view) return; - - const editor = vscode.window.activeTextEditor; - if (!editor) { - vscode.window.showWarningMessage("No active editor"); - return; - } - - const selection = editor.selection; - const selectedText = editor.document.getText(selection); - - if (selectedText) { - const filePath = vscode.workspace.asRelativePath(editor.document.uri); - this._view.webview.postMessage({ - type: "contextAdded", - context: { - type: "selection", - path: filePath, - startLine: selection.start.line + 1, - text: selectedText - } - }); - } else { - const filePath = vscode.workspace.asRelativePath(editor.document.uri); - const content = editor.document.getText(); - this._view.webview.postMessage({ - type: "contextAdded", - context: { - type: "file", - path: filePath, - text: content.substring(0, 5000) - } - }); - } - } - - private _getEditorContext(): any { - const editor = vscode.window.activeTextEditor; - if (!editor) return undefined; - - const selection = editor.selection; - const selectedText = editor.document.getText(selection); - - if (selectedText) { - return { - selection: { - path: vscode.workspace.asRelativePath(editor.document.uri), - text: selectedText, - startLine: selection.start.line + 1 - } - }; - } - return undefined; - } - - private _updateView() { - if (this._view) { - this._view.webview.html = this._getHtmlForWebview(this._view.webview); - } - } - - private _getHtmlForWebview(webview: vscode.Webview) { - const nonce = getNonce(); - - const messagesHtml = this._messages - .map((m) => { - const isUser = m.role === "user"; - const avatarClass = isUser ? "user-avatar" : "ai-avatar"; - const messageClass = isUser ? "user-message" : "ai-message"; - const avatar = isUser ? "U" : "✦"; - - return ` -
-
${avatar}
-
${escapeHtml(m.content)}
-
- `; - }) - .join(""); - - return ` - - - - - - Product OS Chat - - - -
- ${ - messagesHtml || - ` -
-
-

Product OS Chat

-

Deploy, analyze, and create with AI.

-
- - - -
-
- ` - } -
- -
-
-
-
-
-
- Thinking... -
- -
-
- 📎 - - -
-
-
- - -
- -
-
- - - -`; - } -} - -function getNonce() { - let text = ""; - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} - -function escapeHtml(text: string): string { - return text - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'") - .replace(/\n/g, "
"); -} diff --git a/platform/client-ide/extensions/gcp-productos/src/extension.ts b/platform/client-ide/extensions/gcp-productos/src/extension.ts deleted file mode 100644 index 7433f29..0000000 --- a/platform/client-ide/extensions/gcp-productos/src/extension.ts +++ /dev/null @@ -1,176 +0,0 @@ -import * as vscode from "vscode"; -import { listTools, invokeTool, getRun, addRecentRun, checkConnection } from "./api"; -import { ToolsTreeProvider, ToolItem } from "./toolsTreeView"; -import { RunsTreeProvider, RunItem } from "./runsTreeView"; -import { createStatusBar, updateConnectionStatus, dispose as disposeStatusBar } from "./statusBar"; -import { InvokePanel } from "./invokePanel"; -import { showJson, openRun, showRunDocument } from "./ui"; -import { registerChatParticipant } from "./chatParticipant"; - -export function activate(context: vscode.ExtensionContext) { - console.log("Product OS extension activated"); - - // Register @productos in the native VS Code chat - // This gives us the Copilot-like chat experience for FREE - try { - registerChatParticipant(context); - } catch (e) { - console.log("[Product OS] Chat Participant API not available (requires VS Code 1.90+)"); - } - - // Create tree providers - const toolsProvider = new ToolsTreeProvider(); - const runsProvider = new RunsTreeProvider(); - - // Register tree views - vscode.window.registerTreeDataProvider("productos.tools", toolsProvider); - vscode.window.registerTreeDataProvider("productos.runs", runsProvider); - - // Create status bar - createStatusBar(context); - - // Load tools on startup - toolsProvider.loadTools(); - - // === COMMANDS === - - // Configure backend URL - context.subscriptions.push( - vscode.commands.registerCommand("productos.configure", async () => { - const currentUrl = vscode.workspace.getConfiguration("productos").get("backendUrl"); - const backendUrl = await vscode.window.showInputBox({ - prompt: "Control Plane backend URL", - value: currentUrl as string, - placeHolder: "http://localhost:8080" - }); - if (!backendUrl) return; - - await vscode.workspace.getConfiguration("productos").update( - "backendUrl", - backendUrl, - vscode.ConfigurationTarget.Global - ); - - vscode.window.showInformationMessage(`Product OS backend set: ${backendUrl}`); - updateConnectionStatus(); - toolsProvider.loadTools(); - }) - ); - - // Refresh tools - context.subscriptions.push( - vscode.commands.registerCommand("productos.refresh", async () => { - await toolsProvider.loadTools(); - runsProvider.refresh(); - updateConnectionStatus(); - vscode.window.showInformationMessage("Product OS refreshed"); - }) - ); - - // List tools (JSON view) - context.subscriptions.push( - vscode.commands.registerCommand("productos.tools.list", async () => { - try { - const tools = await listTools(); - await showJson("Tools", tools); - } catch (e: any) { - vscode.window.showErrorMessage(`Failed to list tools: ${e.message}`); - } - }) - ); - - // Invoke tool (quick pick) - context.subscriptions.push( - vscode.commands.registerCommand("productos.tools.invoke", async () => { - try { - const tools = await listTools(); - if (tools.length === 0) { - vscode.window.showWarningMessage("No tools available"); - return; - } - - const pick = await vscode.window.showQuickPick( - tools.map(t => ({ - label: t.name, - description: `[${t.risk}] ${t.description}`, - tool: t - })), - { placeHolder: "Select a tool to invoke" } - ); - - if (!pick) return; - - // Open invoke panel - InvokePanel.createOrShow( - context.extensionUri, - pick.tool, - () => runsProvider.refresh() - ); - } catch (e: any) { - vscode.window.showErrorMessage(`Failed: ${e.message}`); - } - }) - ); - - // Invoke from tree view - context.subscriptions.push( - vscode.commands.registerCommand("productos.tools.invokeFromTree", async (item: ToolItem) => { - if (!item?.tool) { - // No item passed, show quick pick - vscode.commands.executeCommand("productos.tools.invoke"); - return; - } - - InvokePanel.createOrShow( - context.extensionUri, - item.tool, - () => runsProvider.refresh() - ); - }) - ); - - // Open run by ID - context.subscriptions.push( - vscode.commands.registerCommand("productos.runs.open", async () => { - const runId = await vscode.window.showInputBox({ - prompt: "Enter Run ID", - placeHolder: "run_20240101..." - }); - if (!runId) return; - - try { - await openRun(runId); - } catch (e: any) { - vscode.window.showErrorMessage(`Failed to open run: ${e.message}`); - } - }) - ); - - // Open run from tree view - context.subscriptions.push( - vscode.commands.registerCommand("productos.runs.openFromTree", async (item: RunItem) => { - if (!item?.run) return; - - try { - const fullRun = await getRun(item.run.run_id); - await showRunDocument(fullRun); - } catch (e: any) { - vscode.window.showErrorMessage(`Failed to open run: ${e.message}`); - } - }) - ); - - // Watch for config changes - context.subscriptions.push( - vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration("productos")) { - updateConnectionStatus(); - toolsProvider.loadTools(); - } - }) - ); -} - -export function deactivate() { - disposeStatusBar(); -} diff --git a/platform/client-ide/extensions/gcp-productos/src/invokePanel.ts b/platform/client-ide/extensions/gcp-productos/src/invokePanel.ts deleted file mode 100644 index 3e33d86..0000000 --- a/platform/client-ide/extensions/gcp-productos/src/invokePanel.ts +++ /dev/null @@ -1,373 +0,0 @@ -import * as vscode from "vscode"; -import { Tool, invokeTool, getRun, addRecentRun } from "./api"; - -export class InvokePanel { - public static currentPanel: InvokePanel | undefined; - private readonly _panel: vscode.WebviewPanel; - private readonly _extensionUri: vscode.Uri; - private _tool: Tool; - private _disposables: vscode.Disposable[] = []; - private _onRunComplete: () => void; - - public static createOrShow( - extensionUri: vscode.Uri, - tool: Tool, - onRunComplete: () => void - ) { - const column = vscode.window.activeTextEditor?.viewColumn ?? vscode.ViewColumn.One; - - if (InvokePanel.currentPanel) { - InvokePanel.currentPanel._tool = tool; - InvokePanel.currentPanel._onRunComplete = onRunComplete; - InvokePanel.currentPanel._update(); - InvokePanel.currentPanel._panel.reveal(column); - return; - } - - const panel = vscode.window.createWebviewPanel( - "productosInvoke", - `Invoke: ${tool.name}`, - column, - { - enableScripts: true, - retainContextWhenHidden: true - } - ); - - InvokePanel.currentPanel = new InvokePanel(panel, extensionUri, tool, onRunComplete); - } - - private constructor( - panel: vscode.WebviewPanel, - extensionUri: vscode.Uri, - tool: Tool, - onRunComplete: () => void - ) { - this._panel = panel; - this._extensionUri = extensionUri; - this._tool = tool; - this._onRunComplete = onRunComplete; - - this._update(); - - this._panel.onDidDispose(() => this.dispose(), null, this._disposables); - - this._panel.webview.onDidReceiveMessage( - async (message) => { - switch (message.command) { - case "invoke": - await this._handleInvoke(message.input, message.dryRun); - break; - case "close": - this._panel.dispose(); - break; - } - }, - null, - this._disposables - ); - } - - private async _handleInvoke(inputText: string, dryRun: boolean) { - try { - const input = JSON.parse(inputText); - - this._panel.webview.postMessage({ command: "invoking" }); - - const result = await invokeTool(this._tool.name, input, dryRun); - - // Fetch full run details - const fullRun = await getRun(result.run_id); - addRecentRun(fullRun); - this._onRunComplete(); - - this._panel.webview.postMessage({ - command: "result", - run: fullRun - }); - - } catch (e: any) { - this._panel.webview.postMessage({ - command: "error", - message: e.message - }); - } - } - - private _update() { - this._panel.title = `Invoke: ${this._tool.name}`; - this._panel.webview.html = this._getHtml(); - } - - private _getHtml(): string { - const tool = this._tool; - const schemaStr = JSON.stringify(tool.inputSchema, null, 2); - const defaultInput = this._generateDefaultInput(tool.inputSchema); - - return ` - - - - - Invoke ${tool.name} - - - -

- ${tool.name} - ${tool.risk} risk -

-

${tool.description}

- - - - -
- - - -
- -
- -
- Input Schema -
${schemaStr}
-
- - - -`; - } - - private _generateDefaultInput(schema: any): string { - if (!schema || schema.type !== "object") return "{}"; - - const obj: any = {}; - const props = schema.properties || {}; - const required = schema.required || []; - - for (const key of required) { - const prop = props[key]; - if (!prop) continue; - - if (prop.type === "string") { - obj[key] = prop.enum ? prop.enum[0] : ""; - } else if (prop.type === "integer" || prop.type === "number") { - obj[key] = prop.minimum ?? 0; - } else if (prop.type === "boolean") { - obj[key] = false; - } else if (prop.type === "array") { - obj[key] = []; - } else if (prop.type === "object") { - obj[key] = {}; - } - } - - return JSON.stringify(obj, null, 2); - } - - public dispose() { - InvokePanel.currentPanel = undefined; - this._panel.dispose(); - while (this._disposables.length) { - const d = this._disposables.pop(); - if (d) d.dispose(); - } - } -} diff --git a/platform/client-ide/extensions/gcp-productos/src/runsTreeView.ts b/platform/client-ide/extensions/gcp-productos/src/runsTreeView.ts deleted file mode 100644 index 0fe3960..0000000 --- a/platform/client-ide/extensions/gcp-productos/src/runsTreeView.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as vscode from "vscode"; -import { Run, getRecentRuns } from "./api"; - -export class RunsTreeProvider implements vscode.TreeDataProvider { - private _onDidChangeTreeData = new vscode.EventEmitter(); - readonly onDidChangeTreeData = this._onDidChangeTreeData.event; - - refresh(): void { - this._onDidChangeTreeData.fire(undefined); - } - - getTreeItem(element: RunItem): vscode.TreeItem { - return element; - } - - getChildren(element?: RunItem): RunItem[] { - if (element) return []; - return getRecentRuns().map(run => new RunItem(run)); - } -} - -export class RunItem extends vscode.TreeItem { - constructor(public readonly run: Run) { - super(run.tool, vscode.TreeItemCollapsibleState.None); - - const statusIcon = run.status === "succeeded" ? "✅" : - run.status === "failed" ? "❌" : - run.status === "running" ? "🔄" : "⏳"; - - this.description = `${statusIcon} ${run.status}`; - - const time = new Date(run.created_at).toLocaleTimeString(); - this.tooltip = new vscode.MarkdownString( - `**${run.tool}**\n\n` + - `Status: ${run.status}\n\n` + - `Run ID: \`${run.run_id}\`\n\n` + - `Time: ${time}` - ); - - this.contextValue = "run"; - - // Icon based on status - const iconId = run.status === "succeeded" ? "pass" : - run.status === "failed" ? "error" : - run.status === "running" ? "sync~spin" : "clock"; - this.iconPath = new vscode.ThemeIcon(iconId); - - this.command = { - command: "productos.runs.openFromTree", - title: "View Run", - arguments: [this] - }; - } -} diff --git a/platform/client-ide/extensions/gcp-productos/src/statusBar.ts b/platform/client-ide/extensions/gcp-productos/src/statusBar.ts deleted file mode 100644 index b272a6d..0000000 --- a/platform/client-ide/extensions/gcp-productos/src/statusBar.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as vscode from "vscode"; -import { checkConnection, getBackendUrl } from "./api"; - -let statusBarItem: vscode.StatusBarItem; -let checkInterval: NodeJS.Timeout | undefined; - -export function createStatusBar(context: vscode.ExtensionContext): vscode.StatusBarItem { - statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); - statusBarItem.command = "productos.configure"; - statusBarItem.text = "$(cloud) Product OS"; - statusBarItem.tooltip = "Click to configure Product OS"; - statusBarItem.show(); - - context.subscriptions.push(statusBarItem); - - // Check connection periodically - updateConnectionStatus(); - checkInterval = setInterval(updateConnectionStatus, 30000); - - return statusBarItem; -} - -export async function updateConnectionStatus(): Promise { - const connected = await checkConnection(); - - if (connected) { - statusBarItem.text = "$(cloud) Product OS"; - statusBarItem.backgroundColor = undefined; - statusBarItem.tooltip = `Connected to ${getBackendUrl()}`; - } else { - statusBarItem.text = "$(cloud-offline) Product OS"; - statusBarItem.backgroundColor = new vscode.ThemeColor("statusBarItem.errorBackground"); - statusBarItem.tooltip = `Disconnected - Click to configure`; - } -} - -export function dispose(): void { - if (checkInterval) { - clearInterval(checkInterval); - } -} diff --git a/platform/client-ide/extensions/gcp-productos/src/toolsTreeView.ts b/platform/client-ide/extensions/gcp-productos/src/toolsTreeView.ts deleted file mode 100644 index 1421826..0000000 --- a/platform/client-ide/extensions/gcp-productos/src/toolsTreeView.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as vscode from "vscode"; -import { Tool, listTools } from "./api"; - -export class ToolsTreeProvider implements vscode.TreeDataProvider { - private _onDidChangeTreeData = new vscode.EventEmitter(); - readonly onDidChangeTreeData = this._onDidChangeTreeData.event; - - private tools: Tool[] = []; - - refresh(): void { - this._onDidChangeTreeData.fire(undefined); - } - - async loadTools(): Promise { - try { - this.tools = await listTools(); - this.refresh(); - } catch (e) { - this.tools = []; - this.refresh(); - } - } - - getTreeItem(element: ToolItem): vscode.TreeItem { - return element; - } - - async getChildren(element?: ToolItem): Promise { - if (element) return []; - - if (this.tools.length === 0) { - await this.loadTools(); - } - - return this.tools.map(tool => new ToolItem(tool)); - } -} - -export class ToolItem extends vscode.TreeItem { - constructor(public readonly tool: Tool) { - super(tool.name, vscode.TreeItemCollapsibleState.None); - - this.description = tool.description; - this.tooltip = new vscode.MarkdownString( - `**${tool.name}**\n\n${tool.description}\n\n` + - `Risk: \`${tool.risk}\`\n\n` + - `Executor: \`${tool.executor.url}${tool.executor.path}\`` - ); - - this.contextValue = "tool"; - - // Icon based on risk level - const iconColor = tool.risk === "high" ? "red" : tool.risk === "medium" ? "orange" : "green"; - this.iconPath = new vscode.ThemeIcon("symbol-method", new vscode.ThemeColor(`charts.${iconColor}`)); - - // Click to invoke - this.command = { - command: "productos.tools.invokeFromTree", - title: "Invoke Tool", - arguments: [this] - }; - } -} diff --git a/platform/client-ide/extensions/gcp-productos/src/ui.ts b/platform/client-ide/extensions/gcp-productos/src/ui.ts deleted file mode 100644 index 4e92fb2..0000000 --- a/platform/client-ide/extensions/gcp-productos/src/ui.ts +++ /dev/null @@ -1,40 +0,0 @@ -import * as vscode from "vscode"; -import { getRun, Run } from "./api"; - -export async function showJson(title: string, obj: any) { - const doc = await vscode.workspace.openTextDocument({ - content: JSON.stringify(obj, null, 2), - language: "json" - }); - await vscode.window.showTextDocument(doc, { preview: false }); - vscode.window.setStatusBarMessage(title, 3000); -} - -export async function openRun(runId: string) { - const run = await getRun(runId); - await showRunDocument(run); -} - -export async function showRunDocument(run: Run) { - const statusEmoji = run.status === "succeeded" ? "✅" : - run.status === "failed" ? "❌" : - run.status === "running" ? "🔄" : "⏳"; - - const content = `// Run: ${run.run_id} -// Tool: ${run.tool} -// Status: ${statusEmoji} ${run.status} -// Created: ${new Date(run.created_at).toLocaleString()} - -// === INPUT === -${JSON.stringify(run.input, null, 2)} - -// === OUTPUT === -${JSON.stringify(run.output ?? run.error ?? null, null, 2)} -`; - - const doc = await vscode.workspace.openTextDocument({ - content, - language: "jsonc" - }); - await vscode.window.showTextDocument(doc, { preview: false }); -} diff --git a/platform/client-ide/extensions/gcp-productos/tsconfig.json b/platform/client-ide/extensions/gcp-productos/tsconfig.json deleted file mode 100644 index 275b271..0000000 --- a/platform/client-ide/extensions/gcp-productos/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true - } -} diff --git a/platform/contracts/tool-registry.yaml b/platform/contracts/tool-registry.yaml deleted file mode 100644 index 80fbff6..0000000 --- a/platform/contracts/tool-registry.yaml +++ /dev/null @@ -1,398 +0,0 @@ -version: 1 - -tools: - - # ---------------------------- - # CODE / DEPLOYMENT - # ---------------------------- - - cloudrun.deploy_service: - description: Build and deploy a Cloud Run service using Cloud Build. Returns the service URL and deployed revision. - risk: medium - executor: - kind: http - url: https://deploy-executor-REPLACE.a.run.app - path: /execute/cloudrun/deploy - inputSchema: - type: object - additionalProperties: false - required: [service_name, repo, ref, env, region] - properties: - service_name: - type: string - minLength: 1 - description: Cloud Run service name. - repo: - type: string - minLength: 1 - description: Git repo URL (HTTPS). - ref: - type: string - minLength: 1 - description: Git ref (branch/tag/SHA). - env: - type: string - enum: [dev, staging, prod] - region: - type: string - minLength: 1 - description: GCP region for the Cloud Run service (e.g., us-central1). - outputSchema: - type: object - properties: - service_url: - type: string - revision: - type: string - - cloudrun.get_service_status: - description: Fetch Cloud Run service status including latest revision and URL. - risk: low - executor: - kind: http - url: https://deploy-executor-REPLACE.a.run.app - path: /execute/cloudrun/status - inputSchema: - type: object - additionalProperties: false - required: [service_name, region] - properties: - service_name: - type: string - minLength: 1 - region: - type: string - minLength: 1 - outputSchema: - type: object - properties: - service_name: - type: string - region: - type: string - service_url: - type: string - latest_ready_revision: - type: string - status: - type: string - enum: [ready, deploying, error, unknown] - - logs.tail: - description: Tail recent logs for a Cloud Run service or for a specific run_id. - risk: low - executor: - kind: http - url: https://observability-executor-REPLACE.a.run.app - path: /execute/logs/tail - inputSchema: - type: object - additionalProperties: false - required: [scope, limit] - properties: - scope: - type: string - enum: [service, run] - service_name: - type: string - region: - type: string - run_id: - type: string - limit: - type: integer - minimum: 1 - maximum: 2000 - default: 200 - outputSchema: - type: object - properties: - lines: - type: array - items: - type: object - properties: - timestamp: - type: string - severity: - type: string - text: - type: string - - # ---------------------------- - # COMPANY BRAIN (BRAND + STYLE) - # ---------------------------- - - brand.get_profile: - description: Retrieve the tenant's brand profile (voice, tone, positioning, compliance constraints). - risk: low - executor: - kind: http - url: https://firestore-executor-REPLACE.a.run.app - path: /execute/brand/get_profile - inputSchema: - type: object - additionalProperties: false - required: [profile_id] - properties: - profile_id: - type: string - minLength: 1 - description: Brand profile identifier (e.g., "default"). - outputSchema: - type: object - properties: - profile_id: - type: string - brand: - type: object - - brand.update_profile: - description: Update the tenant's brand profile. Write operations should be validated and audited. - risk: medium - executor: - kind: http - url: https://firestore-executor-REPLACE.a.run.app - path: /execute/brand/update_profile - inputSchema: - type: object - additionalProperties: false - required: [profile_id, patch] - properties: - profile_id: - type: string - minLength: 1 - patch: - type: object - description: Partial update object; executor must validate allowed fields. - outputSchema: - type: object - properties: - ok: - type: boolean - updated_at: - type: string - - # ---------------------------- - # ANALYTICS / CAUSATION - # ---------------------------- - - analytics.funnel_summary: - description: Return funnel metrics for a time window. Uses curated events in BigQuery. - risk: low - executor: - kind: http - url: https://analytics-executor-REPLACE.a.run.app - path: /execute/analytics/funnel_summary - inputSchema: - type: object - additionalProperties: false - required: [range_days, funnel] - properties: - range_days: - type: integer - minimum: 1 - maximum: 365 - funnel: - type: object - required: [name, steps] - properties: - name: - type: string - steps: - type: array - minItems: 2 - items: - type: object - required: [event_name] - properties: - event_name: - type: string - outputSchema: - type: object - properties: - funnel_name: - type: string - range_days: - type: integer - steps: - type: array - - analytics.top_drivers: - description: Identify top correlated drivers for a target metric/event. - risk: low - executor: - kind: http - url: https://analytics-executor-REPLACE.a.run.app - path: /execute/analytics/top_drivers - inputSchema: - type: object - additionalProperties: false - required: [range_days, target] - properties: - range_days: - type: integer - minimum: 1 - maximum: 365 - target: - type: object - required: [metric] - properties: - metric: - type: string - event_name: - type: string - outputSchema: - type: object - properties: - target: - type: object - range_days: - type: integer - drivers: - type: array - - analytics.write_insight: - description: Persist an insight object (BigQuery table + Firestore pointer + GCS artifact). - risk: medium - executor: - kind: http - url: https://analytics-executor-REPLACE.a.run.app - path: /execute/analytics/write_insight - inputSchema: - type: object - additionalProperties: false - required: [insight] - properties: - insight: - type: object - required: [type, title, summary, severity, confidence, window, recommendations] - properties: - type: - type: string - enum: [funnel_drop, anomaly, driver, experiment_result, general] - title: - type: string - summary: - type: string - severity: - type: string - enum: [info, low, medium, high, critical] - confidence: - type: number - minimum: 0 - maximum: 1 - window: - type: object - recommendations: - type: array - outputSchema: - type: object - properties: - insight_id: - type: string - stored: - type: object - - # ---------------------------- - # MARKETING (GENERATION + PUBLISH) - # ---------------------------- - - marketing.generate_channel_posts: - description: Generate platform-specific social posts from a campaign brief + brand profile. - risk: low - executor: - kind: http - url: https://marketing-executor-REPLACE.a.run.app - path: /execute/marketing/generate_channel_posts - inputSchema: - type: object - additionalProperties: false - required: [brief, channels, brand_profile_id] - properties: - brand_profile_id: - type: string - brief: - type: object - required: [goal, product, audience, key_points] - properties: - goal: - type: string - product: - type: string - audience: - type: string - key_points: - type: array - items: - type: string - channels: - type: array - items: - type: string - enum: [x, linkedin, facebook, instagram, tiktok, youtube, pinterest, reddit] - variations_per_channel: - type: integer - minimum: 1 - maximum: 10 - default: 3 - outputSchema: - type: object - properties: - channels: - type: array - - marketing.publish_missinglettr: - description: Publish or schedule a campaign via Missinglettr. - risk: medium - executor: - kind: http - url: https://marketing-executor-REPLACE.a.run.app - path: /execute/marketing/publish_missinglettr - inputSchema: - type: object - additionalProperties: false - required: [campaign, schedule] - properties: - campaign: - type: object - required: [name, posts] - properties: - name: - type: string - posts: - type: array - items: - type: object - required: [channel, text] - properties: - channel: - type: string - text: - type: string - media_urls: - type: array - items: - type: string - schedule: - type: object - required: [mode] - properties: - mode: - type: string - enum: [now, scheduled] - start_time: - type: string - timezone: - type: string - default: UTC - outputSchema: - type: object - properties: - provider: - type: string - campaign_id: - type: string - status: - type: string - enum: [queued, scheduled, published, failed] diff --git a/platform/docker-compose.yml b/platform/docker-compose.yml deleted file mode 100644 index e77e4db..0000000 --- a/platform/docker-compose.yml +++ /dev/null @@ -1,41 +0,0 @@ -version: '3.8' - -services: - # Firestore Emulator - firestore: - image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators - command: gcloud emulators firestore start --host-port=0.0.0.0:8081 - ports: - - "8081:8081" - - # GCS Emulator (fake-gcs-server) - gcs: - image: fsouza/fake-gcs-server - command: -scheme http -port 4443 - ports: - - "4443:4443" - volumes: - - gcs-data:/data - - # Control Plane API - control-plane: - build: - context: ./backend/control-plane - dockerfile: Dockerfile - ports: - - "8080:8080" - environment: - - PORT=8080 - - GCP_PROJECT_ID=productos-local - - GCS_BUCKET_ARTIFACTS=productos-artifacts-local - - FIRESTORE_COLLECTION_RUNS=runs - - FIRESTORE_COLLECTION_TOOLS=tools - - AUTH_MODE=dev - - FIRESTORE_EMULATOR_HOST=firestore:8081 - - STORAGE_EMULATOR_HOST=http://gcs:4443 - depends_on: - - firestore - - gcs - -volumes: - gcs-data: diff --git a/platform/docs/GETTING_STARTED.md b/platform/docs/GETTING_STARTED.md deleted file mode 100644 index 9240192..0000000 --- a/platform/docs/GETTING_STARTED.md +++ /dev/null @@ -1,143 +0,0 @@ -# Product OS - Getting Started - -## Project Structure - -``` -platform/ -├── backend/ -│ └── control-plane/ # Fastify API server -├── client-ide/ -│ └── extensions/ -│ └── gcp-productos/ # VSCodium/VS Code extension -├── contracts/ # Tool registry schemas -├── infra/ -│ └── terraform/ # GCP infrastructure -├── docs/ # Documentation -└── docker-compose.yml # Local development -``` - -## Quick Start (Local Development) - -### Prerequisites - -- Node.js 22+ -- Docker & Docker Compose -- (Optional) VS Code or VSCodium for extension development - -### 1. Start Local Services - -```bash -cd platform -docker-compose up -d -``` - -This starts: -- Firestore emulator on port 8081 -- GCS emulator on port 4443 -- Control Plane API on port 8080 - -### 2. Run Control Plane in Dev Mode - -For faster iteration without Docker: - -```bash -cd platform/backend/control-plane -cp env.example .env -npm install -npm run dev -``` - -### 3. Test the API - -```bash -# Health check -curl http://localhost:8080/healthz - -# List tools (empty initially) -curl http://localhost:8080/tools - -# Invoke a tool (dry run) -curl -X POST http://localhost:8080/tools/invoke \ - -H "Content-Type: application/json" \ - -d '{ - "tool": "cloudrun.deploy_service", - "tenant_id": "t_dev", - "input": {"service_name": "test"}, - "dry_run": true - }' -``` - -### 4. Build & Install the Extension - -```bash -cd platform/client-ide/extensions/gcp-productos -npm install -npm run build -``` - -Then in VS Code / VSCodium: -1. Open Command Palette (Cmd+Shift+P) -2. Run "Developer: Install Extension from Location..." -3. Select the `gcp-productos` folder - -Or use the VSIX package: -```bash -npx vsce package -code --install-extension gcp-productos-0.0.1.vsix -``` - -## Extension Usage - -Once installed, use the Command Palette: - -- **Product OS: Configure Backend** - Set the Control Plane URL -- **Product OS: List Tools** - View available tools -- **Product OS: Invoke Tool** - Execute a tool -- **Product OS: Open Run** - View run details - -## Deploying to GCP - -### 1. Configure Terraform - -```bash -cd platform/infra/terraform -cp terraform.tfvars.example terraform.tfvars -# Edit terraform.tfvars with your project details -``` - -### 2. Build & Push Container - -```bash -cd platform/backend/control-plane - -# Build -docker build -t us-central1-docker.pkg.dev/YOUR_PROJECT/productos/control-plane:latest . - -# Push (requires gcloud auth) -docker push us-central1-docker.pkg.dev/YOUR_PROJECT/productos/control-plane:latest -``` - -### 3. Apply Terraform - -```bash -cd platform/infra/terraform -terraform init -terraform plan -terraform apply -``` - -## Seeding Tools - -To add tools to the registry, you can: - -1. Use the Firestore console to add documents to the `tools` collection -2. Create a seed script that loads `contracts/tool-registry.yaml` -3. Build an admin endpoint (coming in v2) - -## Next Steps - -- [ ] Build Deploy Executor -- [ ] Build Analytics Executor -- [ ] Add Gemini integration -- [ ] Add OAuth/IAP authentication -- [ ] Create Product-Centric UI panels diff --git a/platform/infra/terraform/iam.tf b/platform/infra/terraform/iam.tf deleted file mode 100644 index 7b01f13..0000000 --- a/platform/infra/terraform/iam.tf +++ /dev/null @@ -1,16 +0,0 @@ -# Allow control-plane to write artifacts in GCS -resource "google_storage_bucket_iam_member" "control_plane_bucket_writer" { - bucket = google_storage_bucket.artifacts.name - role = "roles/storage.objectAdmin" - member = "serviceAccount:${google_service_account.control_plane_sa.email}" -} - -# Firestore access for run/tool metadata -resource "google_project_iam_member" "control_plane_firestore" { - project = var.project_id - role = "roles/datastore.user" - member = "serviceAccount:${google_service_account.control_plane_sa.email}" -} - -# Placeholder: executor services will each have their own service accounts. -# Control-plane should be granted roles/run.invoker on each executor service once created. diff --git a/platform/infra/terraform/main.tf b/platform/infra/terraform/main.tf deleted file mode 100644 index e875e74..0000000 --- a/platform/infra/terraform/main.tf +++ /dev/null @@ -1,54 +0,0 @@ -# GCS Bucket for artifacts (logs, AI outputs, patches) -resource "google_storage_bucket" "artifacts" { - name = var.artifact_bucket_name - location = var.region - uniform_bucket_level_access = true - versioning { enabled = true } -} - -# Firestore (Native mode) – requires enabling in console once per project -resource "google_firestore_database" "default" { - name = "(default)" - location_id = var.region - type = "FIRESTORE_NATIVE" -} - -# Service account for Control Plane -resource "google_service_account" "control_plane_sa" { - account_id = "sa-control-plane" - display_name = "Product OS Control Plane" -} - -# Cloud Run service for Control Plane API -resource "google_cloud_run_v2_service" "control_plane" { - name = "control-plane" - location = var.region - - template { - service_account = google_service_account.control_plane_sa.email - - containers { - image = var.control_plane_image - env { - name = "GCP_PROJECT_ID" - value = var.project_id - } - env { - name = "GCS_BUCKET_ARTIFACTS" - value = google_storage_bucket.artifacts.name - } - env { - name = "AUTH_MODE" - value = "dev" - } - } - } -} - -# Public access for dev; prefer IAM auth in production -resource "google_cloud_run_v2_service_iam_member" "control_plane_public" { - name = google_cloud_run_v2_service.control_plane.name - location = var.region - role = "roles/run.invoker" - member = "allUsers" -} diff --git a/platform/infra/terraform/outputs.tf b/platform/infra/terraform/outputs.tf deleted file mode 100644 index 724ede2..0000000 --- a/platform/infra/terraform/outputs.tf +++ /dev/null @@ -1,9 +0,0 @@ -output "control_plane_url" { - value = google_cloud_run_v2_service.control_plane.uri - description = "URL of the Control Plane API" -} - -output "artifact_bucket" { - value = google_storage_bucket.artifacts.name - description = "GCS bucket for artifacts" -} diff --git a/platform/infra/terraform/providers.tf b/platform/infra/terraform/providers.tf deleted file mode 100644 index ace2f1d..0000000 --- a/platform/infra/terraform/providers.tf +++ /dev/null @@ -1,14 +0,0 @@ -terraform { - required_version = ">= 1.5.0" - required_providers { - google = { - source = "hashicorp/google" - version = "~> 5.30" - } - } -} - -provider "google" { - project = var.project_id - region = var.region -} diff --git a/platform/infra/terraform/terraform.tfvars.example b/platform/infra/terraform/terraform.tfvars.example deleted file mode 100644 index 2d7b525..0000000 --- a/platform/infra/terraform/terraform.tfvars.example +++ /dev/null @@ -1,4 +0,0 @@ -project_id = "your-gcp-project-id" -region = "us-central1" -artifact_bucket_name = "productos-artifacts-dev" -control_plane_image = "us-central1-docker.pkg.dev/YOUR_PROJECT/productos/control-plane:latest" diff --git a/platform/infra/terraform/variables.tf b/platform/infra/terraform/variables.tf deleted file mode 100644 index 14ddaf6..0000000 --- a/platform/infra/terraform/variables.tf +++ /dev/null @@ -1,20 +0,0 @@ -variable "project_id" { - type = string - description = "GCP Project ID" -} - -variable "region" { - type = string - default = "us-central1" - description = "GCP region for resources" -} - -variable "artifact_bucket_name" { - type = string - description = "Name for the GCS bucket storing artifacts" -} - -variable "control_plane_image" { - type = string - description = "Container image URI for control-plane (Artifact Registry)." -} diff --git a/platform/scripts/start-all.sh b/platform/scripts/start-all.sh deleted file mode 100644 index f5db86e..0000000 --- a/platform/scripts/start-all.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -# Start all Product OS services for local development - -echo "🚀 Starting Product OS services..." - -cd "$(dirname "$0")/.." - -# Start Control Plane -echo "Starting Control Plane (port 8080)..." -cd backend/control-plane -npm run dev & -CONTROL_PLANE_PID=$! -cd ../.. - -sleep 2 - -# Start Deploy Executor -echo "Starting Deploy Executor (port 8090)..." -cd backend/executors/deploy -npm run dev & -DEPLOY_PID=$! -cd ../../.. - -# Start Analytics Executor -echo "Starting Analytics Executor (port 8091)..." -cd backend/executors/analytics -npm run dev & -ANALYTICS_PID=$! -cd ../../.. - -# Start Marketing Executor -echo "Starting Marketing Executor (port 8093)..." -cd backend/executors/marketing -npm run dev & -MARKETING_PID=$! -cd ../../.. - -echo "" -echo "✅ All services started!" -echo "" -echo "Services:" -echo " - Control Plane: http://localhost:8080" -echo " - Deploy Executor: http://localhost:8090" -echo " - Analytics Executor: http://localhost:8091" -echo " - Marketing Executor: http://localhost:8093" -echo "" -echo "Press Ctrl+C to stop all services" - -# Wait for any process to exit -wait - -# Cleanup -kill $CONTROL_PLANE_PID $DEPLOY_PID $ANALYTICS_PID $MARKETING_PID 2>/dev/null diff --git a/platform/scripts/templates/turborepo/.gitignore b/platform/scripts/templates/turborepo/.gitignore deleted file mode 100644 index 09507b1..0000000 --- a/platform/scripts/templates/turborepo/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# Dependencies -node_modules -.pnp -.pnp.js - -# Build outputs -dist -.next -out -build -storybook-static - -# Turbo -.turbo - -# Environment -.env -.env.local -.env.*.local - -# Logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* - -# OS -.DS_Store -Thumbs.db - -# Editor -.vscode/settings.json -.idea - -# Testing -coverage -.nyc_output - -# TypeScript -*.tsbuildinfo diff --git a/platform/scripts/templates/turborepo/README.md b/platform/scripts/templates/turborepo/README.md deleted file mode 100644 index 837fa6d..0000000 --- a/platform/scripts/templates/turborepo/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# {{project-name}} - -A full-stack monorepo powered by [Turborepo](https://turbo.build). - -## Structure - -``` -apps/ - product/ — core user-facing application - website/ — marketing and landing pages - admin/ — internal admin tooling - storybook/ — component browser and design system -packages/ - ui/ — shared React component library - tokens/ — design tokens (colors, typography, spacing) - types/ — shared TypeScript types - config/ — shared eslint and tsconfig base configs -``` - -## Getting Started - -```bash -pnpm install -pnpm dev -``` - -## Running a specific app - -```bash -turbo run dev --filter=product -turbo run dev --filter=website -turbo run dev --filter=admin -``` - -## Building - -```bash -pnpm build -# or a single app -turbo run build --filter=product -``` - -## Adding a new app - -```bash -cd apps -npx create-next-app@latest my-new-app -# then add it to the workspace — pnpm will pick it up automatically -``` - -## Adding a new shared package - -```bash -mkdir packages/my-package -# add a package.json with name "@{{project-slug}}/my-package" -# reference it from any app: "@{{project-slug}}/my-package": "workspace:*" -``` diff --git a/platform/scripts/templates/turborepo/apps/admin/next.config.ts b/platform/scripts/templates/turborepo/apps/admin/next.config.ts deleted file mode 100644 index 8db7840..0000000 --- a/platform/scripts/templates/turborepo/apps/admin/next.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - transpilePackages: [ - "@{{project-slug}}/ui", - "@{{project-slug}}/tokens", - ], -}; - -export default nextConfig; diff --git a/platform/scripts/templates/turborepo/apps/admin/package.json b/platform/scripts/templates/turborepo/apps/admin/package.json deleted file mode 100644 index 726b4a9..0000000 --- a/platform/scripts/templates/turborepo/apps/admin/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@{{project-slug}}/admin", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --port 3002", - "build": "next build", - "start": "next start", - "lint": "next lint", - "type-check": "tsc --noEmit" - }, - "dependencies": { - "@{{project-slug}}/ui": "workspace:*", - "@{{project-slug}}/tokens": "workspace:*", - "@{{project-slug}}/types": "workspace:*", - "next": "^15.1.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@{{project-slug}}/config": "workspace:*", - "@types/node": "^22.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "typescript": "^5.7.0" - } -} diff --git a/platform/scripts/templates/turborepo/apps/admin/tsconfig.json b/platform/scripts/templates/turborepo/apps/admin/tsconfig.json deleted file mode 100644 index 3a8e4ad..0000000 --- a/platform/scripts/templates/turborepo/apps/admin/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@{{project-slug}}/config/tsconfig.base.json", - "compilerOptions": { - "plugins": [{ "name": "next" }], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/platform/scripts/templates/turborepo/apps/product/next.config.ts b/platform/scripts/templates/turborepo/apps/product/next.config.ts deleted file mode 100644 index 8db7840..0000000 --- a/platform/scripts/templates/turborepo/apps/product/next.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - transpilePackages: [ - "@{{project-slug}}/ui", - "@{{project-slug}}/tokens", - ], -}; - -export default nextConfig; diff --git a/platform/scripts/templates/turborepo/apps/product/package.json b/platform/scripts/templates/turborepo/apps/product/package.json deleted file mode 100644 index 3c857d9..0000000 --- a/platform/scripts/templates/turborepo/apps/product/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@{{project-slug}}/product", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --port 3000", - "build": "next build", - "start": "next start", - "lint": "next lint", - "type-check": "tsc --noEmit" - }, - "dependencies": { - "@{{project-slug}}/ui": "workspace:*", - "@{{project-slug}}/tokens": "workspace:*", - "@{{project-slug}}/types": "workspace:*", - "next": "^15.1.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@{{project-slug}}/config": "workspace:*", - "@types/node": "^22.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "typescript": "^5.7.0" - } -} diff --git a/platform/scripts/templates/turborepo/apps/product/tsconfig.json b/platform/scripts/templates/turborepo/apps/product/tsconfig.json deleted file mode 100644 index 3a8e4ad..0000000 --- a/platform/scripts/templates/turborepo/apps/product/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@{{project-slug}}/config/tsconfig.base.json", - "compilerOptions": { - "plugins": [{ "name": "next" }], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/platform/scripts/templates/turborepo/apps/storybook/package.json b/platform/scripts/templates/turborepo/apps/storybook/package.json deleted file mode 100644 index cb1eff6..0000000 --- a/platform/scripts/templates/turborepo/apps/storybook/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@{{project-slug}}/storybook", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "storybook dev --port 6006", - "build": "storybook build --output-dir storybook-static", - "type-check": "tsc --noEmit" - }, - "dependencies": { - "@{{project-slug}}/ui": "workspace:*", - "@{{project-slug}}/tokens": "workspace:*", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@{{project-slug}}/config": "workspace:*", - "@chromatic-com/storybook": "^3.0.0", - "@storybook/addon-essentials": "^8.5.0", - "@storybook/addon-interactions": "^8.5.0", - "@storybook/addon-links": "^8.5.0", - "@storybook/blocks": "^8.5.0", - "@storybook/react": "^8.5.0", - "@storybook/react-vite": "^8.5.0", - "@storybook/test": "^8.5.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "storybook": "^8.5.0", - "typescript": "^5.7.0", - "vite": "^6.0.0" - } -} diff --git a/platform/scripts/templates/turborepo/apps/storybook/tsconfig.json b/platform/scripts/templates/turborepo/apps/storybook/tsconfig.json deleted file mode 100644 index aad30ba..0000000 --- a/platform/scripts/templates/turborepo/apps/storybook/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "@{{project-slug}}/config/tsconfig.base.json", - "compilerOptions": { - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "storybook-static"] -} diff --git a/platform/scripts/templates/turborepo/apps/website/next.config.ts b/platform/scripts/templates/turborepo/apps/website/next.config.ts deleted file mode 100644 index 8db7840..0000000 --- a/platform/scripts/templates/turborepo/apps/website/next.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - transpilePackages: [ - "@{{project-slug}}/ui", - "@{{project-slug}}/tokens", - ], -}; - -export default nextConfig; diff --git a/platform/scripts/templates/turborepo/apps/website/package.json b/platform/scripts/templates/turborepo/apps/website/package.json deleted file mode 100644 index c38506c..0000000 --- a/platform/scripts/templates/turborepo/apps/website/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@{{project-slug}}/website", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --port 3001", - "build": "next build", - "start": "next start", - "lint": "next lint", - "type-check": "tsc --noEmit" - }, - "dependencies": { - "@{{project-slug}}/ui": "workspace:*", - "@{{project-slug}}/tokens": "workspace:*", - "@{{project-slug}}/types": "workspace:*", - "next": "^15.1.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@{{project-slug}}/config": "workspace:*", - "@types/node": "^22.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "typescript": "^5.7.0" - } -} diff --git a/platform/scripts/templates/turborepo/apps/website/tsconfig.json b/platform/scripts/templates/turborepo/apps/website/tsconfig.json deleted file mode 100644 index 3a8e4ad..0000000 --- a/platform/scripts/templates/turborepo/apps/website/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "@{{project-slug}}/config/tsconfig.base.json", - "compilerOptions": { - "plugins": [{ "name": "next" }], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/platform/scripts/templates/turborepo/package.json b/platform/scripts/templates/turborepo/package.json deleted file mode 100644 index 2f56868..0000000 --- a/platform/scripts/templates/turborepo/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "{{project-slug}}", - "private": true, - "scripts": { - "build": "turbo run build", - "dev": "turbo run dev", - "lint": "turbo run lint", - "type-check": "turbo run type-check", - "test": "turbo run test", - "clean": "turbo run clean && rm -rf node_modules" - }, - "devDependencies": { - "turbo": "^2.3.3" - }, - "packageManager": "pnpm@9.15.0", - "workspaces": [ - "apps/*", - "packages/*" - ] -} diff --git a/platform/scripts/templates/turborepo/packages/config/eslint.config.js b/platform/scripts/templates/turborepo/packages/config/eslint.config.js deleted file mode 100644 index bb4263c..0000000 --- a/platform/scripts/templates/turborepo/packages/config/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import js from "@eslint/js"; -import tseslint from "typescript-eslint"; -import reactPlugin from "eslint-plugin-react"; -import reactHooksPlugin from "eslint-plugin-react-hooks"; - -/** @type {import("typescript-eslint").Config} */ -export default tseslint.config( - js.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - { - plugins: { - react: reactPlugin, - "react-hooks": reactHooksPlugin, - }, - rules: { - "react/react-in-jsx-scope": "off", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", - "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], - "@typescript-eslint/consistent-type-imports": "error", - }, - } -); diff --git a/platform/scripts/templates/turborepo/packages/config/package.json b/platform/scripts/templates/turborepo/packages/config/package.json deleted file mode 100644 index abaa001..0000000 --- a/platform/scripts/templates/turborepo/packages/config/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@{{project-slug}}/config", - "version": "0.1.0", - "private": true, - "exports": { - "./tsconfig.base.json": "./tsconfig.base.json", - "./eslint": "./eslint.config.js" - } -} diff --git a/platform/scripts/templates/turborepo/packages/tokens/package.json b/platform/scripts/templates/turborepo/packages/tokens/package.json deleted file mode 100644 index 36ebd17..0000000 --- a/platform/scripts/templates/turborepo/packages/tokens/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@{{project-slug}}/tokens", - "version": "0.1.0", - "private": true, - "type": "module", - "exports": { - ".": "./src/index.ts", - "./css": "./src/tokens.css" - }, - "scripts": { - "type-check": "tsc --noEmit", - "lint": "eslint ." - }, - "devDependencies": { - "@{{project-slug}}/config": "workspace:*", - "typescript": "^5.7.0" - } -} diff --git a/platform/scripts/templates/turborepo/packages/tokens/src/index.ts b/platform/scripts/templates/turborepo/packages/tokens/src/index.ts deleted file mode 100644 index b244d49..0000000 --- a/platform/scripts/templates/turborepo/packages/tokens/src/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -export const colors = { - brand: { - 50: "#f0f9ff", - 100: "#e0f2fe", - 200: "#bae6fd", - 300: "#7dd3fc", - 400: "#38bdf8", - 500: "#0ea5e9", - 600: "#0284c7", - 700: "#0369a1", - 800: "#075985", - 900: "#0c4a6e", - }, - neutral: { - 50: "#fafafa", - 100: "#f4f4f5", - 200: "#e4e4e7", - 300: "#d4d4d8", - 400: "#a1a1aa", - 500: "#71717a", - 600: "#52525b", - 700: "#3f3f46", - 800: "#27272a", - 900: "#18181b", - }, - success: { DEFAULT: "#22c55e", light: "#dcfce7", dark: "#15803d" }, - warning: { DEFAULT: "#f59e0b", light: "#fef3c7", dark: "#b45309" }, - error: { DEFAULT: "#ef4444", light: "#fee2e2", dark: "#b91c1c" }, -} as const; - -export const typography = { - fontFamily: { - sans: "var(--font-sans, ui-sans-serif, system-ui, sans-serif)", - mono: "var(--font-mono, ui-monospace, monospace)", - }, - fontSize: { - xs: ["0.75rem", { lineHeight: "1rem" }], - sm: ["0.875rem", { lineHeight: "1.25rem" }], - base: ["1rem", { lineHeight: "1.5rem" }], - lg: ["1.125rem", { lineHeight: "1.75rem" }], - xl: ["1.25rem", { lineHeight: "1.75rem" }], - "2xl":["1.5rem", { lineHeight: "2rem" }], - "3xl":["1.875rem", { lineHeight: "2.25rem" }], - "4xl":["2.25rem", { lineHeight: "2.5rem" }], - }, -} as const; - -export const spacing = { - px: "1px", - 0: "0", - 1: "0.25rem", - 2: "0.5rem", - 3: "0.75rem", - 4: "1rem", - 5: "1.25rem", - 6: "1.5rem", - 8: "2rem", - 10: "2.5rem", - 12: "3rem", - 16: "4rem", - 20: "5rem", - 24: "6rem", - 32: "8rem", -} as const; - -export const radius = { - none: "0", - sm: "0.125rem", - DEFAULT: "0.25rem", - md: "0.375rem", - lg: "0.5rem", - xl: "0.75rem", - "2xl":"1rem", - full: "9999px", -} as const; - -export const shadows = { - sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)", - DEFAULT: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", - md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", - lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)", - xl: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)", - none:"none", -} as const; diff --git a/platform/scripts/templates/turborepo/packages/tokens/src/tokens.css b/platform/scripts/templates/turborepo/packages/tokens/src/tokens.css deleted file mode 100644 index 40354f5..0000000 --- a/platform/scripts/templates/turborepo/packages/tokens/src/tokens.css +++ /dev/null @@ -1,46 +0,0 @@ -:root { - /* Brand */ - --color-brand-50: #f0f9ff; - --color-brand-100: #e0f2fe; - --color-brand-200: #bae6fd; - --color-brand-300: #7dd3fc; - --color-brand-400: #38bdf8; - --color-brand-500: #0ea5e9; - --color-brand-600: #0284c7; - --color-brand-700: #0369a1; - --color-brand-800: #075985; - --color-brand-900: #0c4a6e; - - /* Neutral */ - --color-neutral-50: #fafafa; - --color-neutral-100: #f4f4f5; - --color-neutral-200: #e4e4e7; - --color-neutral-300: #d4d4d8; - --color-neutral-400: #a1a1aa; - --color-neutral-500: #71717a; - --color-neutral-600: #52525b; - --color-neutral-700: #3f3f46; - --color-neutral-800: #27272a; - --color-neutral-900: #18181b; - - /* Semantic */ - --color-success: #22c55e; - --color-success-light: #dcfce7; - --color-warning: #f59e0b; - --color-warning-light: #fef3c7; - --color-error: #ef4444; - --color-error-light: #fee2e2; - - /* Typography */ - --font-sans: ui-sans-serif, system-ui, sans-serif; - --font-mono: ui-monospace, monospace; - - /* Radius */ - --radius-sm: 0.125rem; - --radius: 0.25rem; - --radius-md: 0.375rem; - --radius-lg: 0.5rem; - --radius-xl: 0.75rem; - --radius-2xl: 1rem; - --radius-full: 9999px; -} diff --git a/platform/scripts/templates/turborepo/packages/types/package.json b/platform/scripts/templates/turborepo/packages/types/package.json deleted file mode 100644 index 7bfae59..0000000 --- a/platform/scripts/templates/turborepo/packages/types/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@{{project-slug}}/types", - "version": "0.1.0", - "private": true, - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "type-check": "tsc --noEmit", - "lint": "eslint ." - }, - "devDependencies": { - "@{{project-slug}}/config": "workspace:*", - "typescript": "^5.7.0" - } -} diff --git a/platform/scripts/templates/turborepo/packages/types/src/index.ts b/platform/scripts/templates/turborepo/packages/types/src/index.ts deleted file mode 100644 index 6356f94..0000000 --- a/platform/scripts/templates/turborepo/packages/types/src/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Shared types for {{project-name}} - * - * Add types here that are used across product, website, and admin. - * Import in any app: import type { User } from "@{{project-slug}}/types" - */ - -export type ID = string; - -export type User = { - id: ID; - email: string; - name: string; - avatarUrl?: string; - createdAt: string; -}; - -export type ApiResponse = { - data: T; - error: null; -} | { - data: null; - error: { - message: string; - code?: string; - }; -}; - -export type PaginatedResponse = { - items: T[]; - total: number; - page: number; - pageSize: number; - hasMore: boolean; -}; diff --git a/platform/scripts/templates/turborepo/packages/ui/package.json b/platform/scripts/templates/turborepo/packages/ui/package.json deleted file mode 100644 index 010d130..0000000 --- a/platform/scripts/templates/turborepo/packages/ui/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@{{project-slug}}/ui", - "version": "0.1.0", - "private": true, - "type": "module", - "exports": { - ".": "./src/index.ts", - "./styles": "./src/styles.css" - }, - "scripts": { - "type-check": "tsc --noEmit", - "lint": "eslint ." - }, - "dependencies": { - "@{{project-slug}}/tokens": "workspace:*", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@{{project-slug}}/config": "workspace:*", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "typescript": "^5.7.0" - }, - "peerDependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0" - } -} diff --git a/platform/scripts/templates/turborepo/packages/ui/src/components/Badge.tsx b/platform/scripts/templates/turborepo/packages/ui/src/components/Badge.tsx deleted file mode 100644 index df20e07..0000000 --- a/platform/scripts/templates/turborepo/packages/ui/src/components/Badge.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type { HTMLAttributes } from "react"; - -type BadgeVariant = "default" | "success" | "warning" | "error" | "brand"; - -interface BadgeProps extends HTMLAttributes { - variant?: BadgeVariant; -} - -const variantClasses: Record = { - default: "bg-[var(--color-neutral-100)] text-[var(--color-neutral-700)]", - success: "bg-[var(--color-success-light)] text-[var(--color-success-dark,#15803d)]", - warning: "bg-[var(--color-warning-light)] text-[var(--color-warning-dark,#b45309)]", - error: "bg-[var(--color-error-light)] text-[var(--color-error-dark,#b91c1c)]", - brand: "bg-[var(--color-brand-100)] text-[var(--color-brand-700)]", -}; - -export function Badge({ variant = "default", className = "", children, ...props }: BadgeProps) { - return ( - - {children} - - ); -} diff --git a/platform/scripts/templates/turborepo/packages/ui/src/components/Button.tsx b/platform/scripts/templates/turborepo/packages/ui/src/components/Button.tsx deleted file mode 100644 index 97a07cd..0000000 --- a/platform/scripts/templates/turborepo/packages/ui/src/components/Button.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import type { ButtonHTMLAttributes } from "react"; - -type Variant = "primary" | "secondary" | "ghost" | "destructive"; -type Size = "sm" | "md" | "lg"; - -interface ButtonProps extends ButtonHTMLAttributes { - variant?: Variant; - size?: Size; - loading?: boolean; -} - -const variantClasses: Record = { - primary: "bg-[var(--color-brand-600)] text-white hover:bg-[var(--color-brand-700)]", - secondary: "bg-[var(--color-neutral-100)] text-[var(--color-neutral-900)] hover:bg-[var(--color-neutral-200)]", - ghost: "bg-transparent text-[var(--color-neutral-700)] hover:bg-[var(--color-neutral-100)]", - destructive: "bg-[var(--color-error)] text-white hover:opacity-90", -}; - -const sizeClasses: Record = { - sm: "px-3 py-1.5 text-sm", - md: "px-4 py-2 text-sm", - lg: "px-5 py-2.5 text-base", -}; - -export function Button({ - variant = "primary", - size = "md", - loading = false, - disabled, - className = "", - children, - ...props -}: ButtonProps) { - return ( - - ); -} diff --git a/platform/scripts/templates/turborepo/packages/ui/src/components/Card.tsx b/platform/scripts/templates/turborepo/packages/ui/src/components/Card.tsx deleted file mode 100644 index 2685766..0000000 --- a/platform/scripts/templates/turborepo/packages/ui/src/components/Card.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { HTMLAttributes } from "react"; - -interface CardProps extends HTMLAttributes { - padding?: "none" | "sm" | "md" | "lg"; -} - -const paddingClasses = { - none: "", - sm: "p-3", - md: "p-5", - lg: "p-8", -}; - -export function Card({ padding = "md", className = "", children, ...props }: CardProps) { - return ( -
- {children} -
- ); -} diff --git a/platform/scripts/templates/turborepo/packages/ui/src/components/Input.tsx b/platform/scripts/templates/turborepo/packages/ui/src/components/Input.tsx deleted file mode 100644 index eb72989..0000000 --- a/platform/scripts/templates/turborepo/packages/ui/src/components/Input.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { InputHTMLAttributes } from "react"; - -interface InputProps extends InputHTMLAttributes { - label?: string; - error?: string; - hint?: string; -} - -export function Input({ label, error, hint, className = "", id, ...props }: InputProps) { - const inputId = id ?? label?.toLowerCase().replace(/\s+/g, "-"); - - return ( -
- {label && ( - - )} - - {error &&

{error}

} - {hint && !error &&

{hint}

} -
- ); -} diff --git a/platform/scripts/templates/turborepo/packages/ui/src/index.ts b/platform/scripts/templates/turborepo/packages/ui/src/index.ts deleted file mode 100644 index 58899a7..0000000 --- a/platform/scripts/templates/turborepo/packages/ui/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { Button } from "./components/Button.js"; -export { Card } from "./components/Card.js"; -export { Input } from "./components/Input.js"; -export { Badge } from "./components/Badge.js"; diff --git a/platform/scripts/templates/turborepo/packages/ui/src/styles.css b/platform/scripts/templates/turborepo/packages/ui/src/styles.css deleted file mode 100644 index 2448116..0000000 --- a/platform/scripts/templates/turborepo/packages/ui/src/styles.css +++ /dev/null @@ -1,2 +0,0 @@ -/* Import design tokens — include this once at your app root */ -@import "@{{project-slug}}/tokens/css"; diff --git a/prd-agent-prompt.pdf b/prd-agent-prompt.pdf new file mode 100644 index 0000000..e77842b Binary files /dev/null and b/prd-agent-prompt.pdf differ diff --git a/preview-assist-ui/index.html b/preview-assist-ui/index.html new file mode 100644 index 0000000..0d2c528 --- /dev/null +++ b/preview-assist-ui/index.html @@ -0,0 +1,12 @@ + + + + + + VIBN Assist Preview + + +
+ + + diff --git a/preview-assist-ui/package-lock.json b/preview-assist-ui/package-lock.json new file mode 100644 index 0000000..b2f8f7b --- /dev/null +++ b/preview-assist-ui/package-lock.json @@ -0,0 +1,1677 @@ +{ + "name": "preview-assist-ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "preview-assist-ui", + "version": "0.0.0", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.0", + "vite": "^5.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/preview-assist-ui/package.json b/preview-assist-ui/package.json new file mode 100644 index 0000000..7c137f6 --- /dev/null +++ b/preview-assist-ui/package.json @@ -0,0 +1,19 @@ +{ + "name": "preview-assist-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.0", + "vite": "^5.0.0" + } +} diff --git a/preview-assist-ui/src/App.jsx b/preview-assist-ui/src/App.jsx new file mode 100644 index 0000000..5afc23d --- /dev/null +++ b/preview-assist-ui/src/App.jsx @@ -0,0 +1,932 @@ +import { useState, useRef, useEffect } from "react"; +import Website from "./Website.jsx"; +import Dashboard from "./Dashboard.jsx"; + +// ─── DESIGN TOKENS ──────────────────────────────────────────────────────────── +const C = { + ink: "#0f172a", ink2: "#1e293b", ink3: "#475569", muted: "#94a3b8", + border: "#f1f5f9", border2: "#e2e8f0", surface: "#f8fafc", white: "#fff", + green: "#22c55e", greenBg: "#f0fdf4", + purple: "#6366f1", purpleBg: "#eef2ff", + amber: "#f59e0b", amberBg: "#fffbeb", amberBorder: "#fde68a", amberText: "#92400e", + blue: "#0ea5e9", blueBg: "#f0f9ff", + rose: "#f43f5e", roseBg: "#fff1f2", + teal: "#14b8a6", tealBg: "#f0fdfa", + violet: "#8b5cf6", violetBg: "#f5f3ff", +}; + +const PHASES = [ + { id: "welcome", label: "Welcome", icon: "◎", desc: "How this works" }, + { id: "discover", label: "Discover", icon: "◇", desc: "Your idea" }, + { id: "architect", label: "Architect", icon: "⬡", desc: "What gets built" }, + { id: "design", label: "Design", icon: "◈", desc: "How it looks" }, + { id: "market", label: "Market", icon: "✦", desc: "How you'll grow" }, + { id: "build", label: "Build MVP", icon: "▲", desc: "Review & launch" }, +]; + +// ─── DISCOVER DATA ──────────────────────────────────────────────────────────── +const QUESTIONS = [ + { key: "idea", q: "Tell me about your idea. What does it do and who is it for?" }, + { key: "problem", q: "What's the painful thing your users are doing today instead?" }, + { key: "users", q: "Describe your ideal first customer in one sentence." }, + { key: "value", q: "What's the one thing they'll love most about your product?" }, + { key: "revenue", q: "How will you charge for it? Subscription, one-time, or free to start?" }, + { key: "features", q: "Name the 3 things users must be able to do in version one." }, +]; + +// ─── ARCHITECT DATA ─────────────────────────────────────────────────────────── +const ARCH_BLOCKS = [ + { id: "frontend", icon: "◇", label: "Frontend", color: C.purple, bg: C.purpleBg, chosen: "Web app", what: "The screens your users see — dashboard, sign up, settings, and everything in between. Works on desktop and mobile.", alts: [{ id: "webapp", label: "Web app", desc: "Runs in any browser, desktop & mobile" }, { id: "mobile", label: "Mobile-first", desc: "Optimised for phones, still works on desktop" }] }, + { id: "backend", icon: "⬡", label: "Backend & Database", color: C.blue, bg: C.blueBg, chosen: "API + database", what: "The engine behind the scenes. Stores all your data securely and runs your product's logic.", alts: [{ id: "api_pg", label: "API + database", desc: "Standard setup, works for almost everything" }, { id: "realtime", label: "Real-time", desc: "If your product needs live updates between users" }] }, + { id: "auth", icon: "◎", label: "Sign up & Login", color: C.rose, bg: C.roseBg, chosen: "Email + social login", what: "How your users create accounts and log in. Getting this right reduces drop-off at the first step.", alts: [{ id: "email_social", label: "Email + social login", desc: "Email/password plus Google & GitHub — recommended" }, { id: "email_only", label: "Email & password only", desc: "Simpler, no third-party login" }, { id: "magic_link", label: "Magic link", desc: "No password — users get a login link by email" }, { id: "sso", label: "SSO for teams", desc: "If you're selling to companies, not individuals" }] }, + { id: "payments", icon: "◈", label: "Payments", color: C.amber, bg: C.amberBg, chosen: "Subscription billing", what: "How you collect money from customers. Set up now so you're ready to charge from day one.", alts: [{ id: "subscription", label: "Subscription billing", desc: "Monthly or annual plans — recommended for SaaS" }, { id: "one_time", label: "One-time purchase", desc: "Pay once, own forever" }, { id: "usage", label: "Usage-based", desc: "Charge based on what users consume" }, { id: "free", label: "Free for now", desc: "No payments yet — add billing later" }] }, + { id: "email", icon: "✦", label: "Email", color: C.teal, bg: C.tealBg, chosen: "Transactional + marketing", what: "Emails sent to your users — welcome messages, password resets, and newsletters when you're ready.", alts: [{ id: "both", label: "Transactional + marketing", desc: "Welcome emails, resets, plus campaign capability" }, { id: "trans", label: "Transactional only", desc: "Just the essential emails, nothing more" }, { id: "none", label: "None for now", desc: "Skip email entirely — add it later" }] }, + { id: "hosting", icon: "▲", label: "Hosting", color: C.violet, bg: C.violetBg, chosen: "Your own servers", what: "Where your product lives. Deployed to your own infrastructure — you own everything, no lock-in.", alts: [{ id: "own", label: "Your own servers", desc: "Coolify + Gitea — already configured" }], locked: true }, +]; + +const PAGES_GENERATED = [ + { group: "Public", color: C.purple, pages: ["Landing page", "Pricing", "About", "Blog"] }, + { group: "Auth", color: C.rose, pages: ["Sign up", "Log in", "Forgot password"] }, + { group: "App", color: C.blue, pages: ["Dashboard", "Onboarding", "Settings", "Invite team"] }, + { group: "Payments", color: C.amber, pages: ["Checkout", "Success", "Manage subscription"] }, +]; + +// ─── DESIGN DATA ────────────────────────────────────────────────────────────── +const DESIGN_FEELS = [ + { id: "clean", label: "Clean & focused", ref: "Like Notion or Linear", bg: "#fff", surface: "#f8fafc", text: "#0f172a", accent: "#6366f1", radius: 8 }, + { id: "bold", label: "Bold & confident", ref: "Like Stripe or Vercel", bg: "#0f172a", surface: "#1e293b", text: "#f8fafc", accent: "#f43f5e", radius: 8 }, + { id: "warm", label: "Warm & friendly", ref: "Like Mailchimp or Basecamp", bg: "#fffbeb", surface: "#fef3c7", text: "#78350f", accent: "#f59e0b", radius: 14 }, + { id: "fresh", label: "Fresh & modern", ref: "Like Loom or Superhuman", bg: "#f0fdf4", surface: "#dcfce7", text: "#14532d", accent: "#22c55e", radius: 10 }, + { id: "electric", label: "Electric & vivid", ref: "Like Figma or Framer", bg: "#faf5ff", surface: "#ede9fe", text: "#4c1d95", accent: "#8b5cf6", radius: 8 }, + { id: "luxury", label: "Premium & refined", ref: "Like Linear or Craft", bg: "#0c0a09", surface: "#1c1917", text: "#f5f5f4", accent: "#d4a853", radius: 6 }, +]; + +// ─── MARKET DATA ───────────────────────────────────────────────────────────── +const VOICE_OPTIONS = [ + { key: "tone", label: "Tone", a: "Friendly & approachable", b: "Professional & authoritative" }, + { key: "style", label: "Style", a: "Conversational & casual", b: "Precise & concise" }, + { key: "personality", label: "Personality", a: "Warm & encouraging", b: "Direct & confident" }, +]; + +const SUGGESTED_TOPICS = () => [ + { id: "t1", title: "The problem we're solving", angle: "Show users you deeply understand their pain before pitching your solution.", channels: ["Blog", "Tweet thread", "LinkedIn", "Email"] }, + { id: "t2", title: "Who this is built for", angle: "Paint a picture of your ideal user — they should read it and think 'that's me'.", channels: ["Blog", "LinkedIn", "Website section"] }, + { id: "t3", title: "Why now is the right time", angle: "What's changed in the market that makes this product possible or necessary?", channels: ["Blog", "Tweet thread", "Email"] }, +]; + +const WEBSITE_FEELS = [ + { id: "editorial", label: "Editorial", ref: "Bold headlines, strong opinions", bg: "#fff", accent: "#ef4444" }, + { id: "startup", label: "Startup energy", ref: "Clear, conversion-focused", bg: "#f8fafc", accent: "#0ea5e9" }, + { id: "minimal", label: "Ultra minimal", ref: "Let the product speak", bg: "#fff", accent: "#111" }, + { id: "warm_w", label: "Warm & human", ref: "Feels personal and trustworthy", bg: "#fff7ed", accent: "#ea580c" }, +]; + +// ─── SHARED UI ──────────────────────────────────────────────────────────────── +function PhaseHeader({ title, desc, action }) { + return ( +
+
+
{title}
+ {desc &&
{desc}
} +
+ {action} +
+ ); +} + +function ContinueBtn({ label, onClick, disabled }) { + return ( + + ); +} + +// ─── LIVE APP MOCKUP ────────────────────────────────────────────────────────── +function AppMockup({ feel }) { + const t = DESIGN_FEELS.find((f) => f.id === feel) || DESIGN_FEELS[0]; + const isDark = t.bg === "#0f172a" || t.bg === "#0c0a09"; + const border = isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)"; + return ( +
+
+
YourApp
+
{["Dashboard", "Settings", "Billing"].map((l) => {l})}
+
Upgrade
+
+
+
Dashboard
+
+ {[["Users", "182"], ["Revenue", "$2.4k"], ["Active", "94%"]].map(([l, v], i) => ( +
+
{l}
+
{v}
+
+ ))} +
+
+
Recent activity
+ {["Alex signed up · 2m ago", "Priya upgraded to Pro · 1h ago", "Dan invited 3 teammates · 3h ago"].map((item, i) => ( +
+
+ {item} +
+ ))} +
+
+
+ ); +} + +// ─── PHASE: WELCOME ─────────────────────────────────────────────────────────── +function WelcomePhase({ onStart }) { + const steps = [ + { icon: "◇", label: "You describe your idea", desc: "A short conversation — no technical knowledge needed." }, + { icon: "⬡", label: "We plan what gets built", desc: "AI maps out your full product architecture in plain language." }, + { icon: "◈", label: "You choose how it looks", desc: "Pick a visual style for your product and marketing." }, + { icon: "✦", label: "You set your market angle", desc: "Define your voice and the topics that'll drive your content." }, + { icon: "▲", label: "We build it", desc: "AI codes everything and deploys to your live URL." }, + ]; + return ( +
+
+
+
+
+ vibn · MVP Builder +
+
+ From idea to live product.
No code needed. +
+
+ Answer a few questions about your idea. AI plans, designs, and builds your full SaaS product — then deploys it to your own servers. Takes about 10 minutes to set up. +
+
+
+ {steps.map((s, i) => ( +
+
{s.icon}
+
+
{s.label}
+
{s.desc}
+
+
+ ))} +
+
+ + ~10 minutes · You own everything at the end +
+
+
+ ); +} + +// ─── PHASE: DISCOVER ───────────────────────────────────────────────────────── +function DiscoverPhase({ onComplete }) { + const [step, setStep] = useState(0); + const [answers, setAnswers] = useState({}); + const [input, setInput] = useState(""); + const [msgs, setMsgs] = useState([{ role: "assistant", text: "Let's start with the big picture.\n\n" + QUESTIONS[0].q }]); + const [done, setDone] = useState(false); + const bottomRef = useRef(null); + useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [msgs]); + + const acks = { idea: "Good — I can work with that.", problem: "That's a real pain worth solving.", users: "Clear. That focus will shape everything.", value: "Strong differentiator.", revenue: "Makes sense for this type of product." }; + + const send = () => { + if (!input.trim()) return; + const val = input.trim(); setInput(""); + const q = QUESTIONS[step]; + const newA = { ...answers, [q.key]: val }; + setAnswers(newA); + setMsgs((m) => [...m, { role: "user", text: val }]); + setTimeout(() => { + const next = step + 1; + if (next < QUESTIONS.length) { + setStep(next); + setMsgs((m) => [...m, { role: "assistant", text: `${acks[q.key] || "Got it."}\n\n${QUESTIONS[next].q}` }]); + } else { + setMsgs((m) => [...m, { role: "assistant", text: "That's all I need.\n\nI've drafted your product plan on the right. Have a read — if it looks right, let's move on to planning what gets built." }]); + setDone(true); + } + }, 400); + }; + + return ( +
+
+ +
+
+ {QUESTIONS.map((_, i) =>
)} +
+
+
+ {msgs.map((msg, i) => ( +
+
{msg.text}
+
+ ))} +
+
+
+ {!done + ?
+ setInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && send()} placeholder="Type your answer…" style={{ flex: 1, border: "none", background: "transparent", fontSize: 14, color: C.ink, outline: "none" }} autoFocus /> + +
+ : + } +
+
+
+
Your product plan
+ {QUESTIONS.map((q, i) => ( +
step ? 0.35 : 1, transition: "all 0.3s" }}> +
{q.key}
+
{answers[q.key] || "Waiting…"}
+
+ ))} +
+
+ ); +} + +// ─── PHASE: ARCHITECT ───────────────────────────────────────────────────────── +function ArchCard({ block, label, onEdit }) { + return ( +
(e.currentTarget.style.boxShadow = "0 4px 20px rgba(0,0,0,0.07)")} + onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}> +
+
+
+
{block.icon}
+
{block.label}
+
+
{block.what}
+
+
+ {label} +
+
+
+ {block.locked ? "Required for your setup" : "AI recommended this"} + +
+
+ ); +} + +function ArchitectPhase({ onComplete }) { + const [choices, setChoices] = useState(() => Object.fromEntries(ARCH_BLOCKS.map((b) => [b.id, b.alts[0].id]))); + const [editing, setEditing] = useState(null); + const editBlock = ARCH_BLOCKS.find((b) => b.id === editing); + const save = (id, val) => { setChoices((c) => ({ ...c, [id]: val })); setEditing(null); }; + const choiceLabel = (block) => block.alts.find((a) => a.id === choices[block.id])?.label || block.alts[0].label; + return ( +
+
+ onComplete(choices)} />} /> +
+
+ {ARCH_BLOCKS.slice(0, 3).map((b) => setEditing(b.id)} />)} +
+
+ {ARCH_BLOCKS.slice(3).map((b) => setEditing(b.id)} />)} +
+
+
+
+
Pages to build
+ {PAGES_GENERATED.map((g) => ( +
+
+
+
{g.group}
+
+ {g.pages.map((p) => ( +
+
{p} +
+ ))} +
+ ))} +
+
◉ Your infrastructure
+
Code pushed to Gitea. Coolify auto-deploys. You own everything.
+
+
+ {editing && editBlock && ( +
+
+
+
{editBlock.icon}
+
+
{editBlock.label}
+
{editBlock.what}
+
+ +
+
+ {editBlock.alts.map((alt) => { + const isSel = choices[editBlock.id] === alt.id; + return ( + + ); + })} +
+
+ + +
+
+
+ )} +
+ ); +} + +// ─── PHASE: DESIGN ──────────────────────────────────────────────────────────── +function DesignPhase({ onComplete }) { + const [feel, setFeel] = useState("clean"); + return ( +
+
+ +
+
+ Pick the feeling you want users to get. You can refine every detail after launch — this sets the foundation. +
+ {DESIGN_FEELS.map((f) => { + const isDark = f.bg === "#0f172a" || f.bg === "#0c0a09"; + const isSel = feel === f.id; + return ( + + ); + })} +
+
+ +
+
+
+
+
{["#f43f5e", "#f59e0b", "#22c55e"].map((c) =>
)}
+ app.yourproduct.com + {DESIGN_FEELS.find((f) => f.id === feel)?.label} +
+
+ +
+
+
+ ); +} + +// ─── PHASE: MARKET ──────────────────────────────────────────────────────────── +function WebsitePreview({ feel }) { + const t = WEBSITE_FEELS.find((f) => f.id === feel) || WEBSITE_FEELS[0]; + const isDark = t.bg === "#0c0a09" || t.bg === "#0f172a"; + const border = isDark ? "rgba(255,255,255,0.07)" : "rgba(0,0,0,0.06)"; + const textCol = isDark ? "#fff" : (t.text || C.ink); + return ( +
+
+
YourApp
+
{["Product", "Blog", "Pricing"].map((l) => {l})}
+
Get started
+
+
+
+
Launch your SaaS in days, not months
+
Describe your idea. AI builds, deploys, and markets your product automatically.
+
+
Start free
+
See demo
+
+
+
+ {[["⬡", "Build", "AI writes every line"], ["◇", "Grow", "Content on autopilot"], ["◈", "Launch", "Live in minutes"]].map(([ic, lb, desc]) => ( +
+
{ic}
+
{lb}
+
{desc}
+
+ ))} +
+
+
+ ); +} + +function MarketPhase({ prd, onComplete }) { + const [tab, setTab] = useState("voice"); + const [voice, setVoice] = useState({ tone: 0.3, style: 0.4, personality: 0.3 }); + const [topics, setTopics] = useState(SUGGESTED_TOPICS()); + const [editingTopic, setEditingTopic] = useState(null); + const [websiteFeel, setWebsiteFeel] = useState("startup"); + + const tabs = ["voice", "topics", "style"]; + const tabLabels = { voice: "Voice", topics: "Topics", style: "Website style" }; + const voiceDesc = (key, val) => { const opt = VOICE_OPTIONS.find((v) => v.key === key); return val < 0.4 ? opt.a : val > 0.6 ? opt.b : "Balanced"; }; + const addTopic = () => { const id = `t${Date.now()}`; setTopics((t) => [...t, { id, title: "", angle: "", channels: ["Blog", "Email"], editing: true }]); setEditingTopic(id); }; + const updateTopic = (id, field, val) => setTopics((t) => t.map((tp) => (tp.id === id ? { ...tp, [field]: val } : tp))); + const removeTopic = (id) => setTopics((t) => t.filter((tp) => tp.id !== id)); + + return ( +
+ onComplete({ voice, topics, websiteFeel })} />} /> +
+ {tabs.map((t) => ( + + ))} +
+
+ {tab === "voice" && ( +
+
+
This defines how your brand sounds — in emails, on your website, in social posts. AI uses this to write content that sounds like you.
+
+ {VOICE_OPTIONS.map((opt) => ( +
+
+
{opt.label}
+
{voiceDesc(opt.key, voice[opt.key])}
+
+
+ {opt.a} + setVoice((v) => ({ ...v, [opt.key]: parseFloat(e.target.value) }))} style={{ flex: 1, accentColor: C.ink, cursor: "pointer" }} /> + {opt.b} +
+
+ ))} +
+
+
How your brand will sound
+
+ {voice.tone < 0.4 ? "Warm and approachable" : voice.tone > 0.6 ? "Professional and authoritative" : "Balanced"} + {" · "}{voice.style < 0.4 ? "conversational" : voice.style > 0.6 ? "precise" : "clear"} + {" · "}{voice.personality < 0.4 ? "encouraging" : voice.personality > 0.6 ? "direct" : "steady"} +
+
+
+
+ )} + {tab === "topics" && ( +
+
+
Topics are the campaign angles your content will be built around. Each topic generates a blog post, tweet thread, LinkedIn post, and email. AI suggested these from your PRD — edit or add your own.
+
+ {topics.map((topic) => ( +
+ {editingTopic === topic.id ? ( +
+ updateTopic(topic.id, "title", e.target.value)} placeholder="Topic title" style={{ fontSize: 14, fontWeight: 600, color: C.ink, border: `1px solid ${C.border2}`, borderRadius: 8, padding: "9px 12px", outline: "none", background: C.surface }} autoFocus /> +