← back to Ventura Claw
scripts/auto-build.sh
357 lines
#!/usr/bin/env bash
# VenturaClaw — Auto-Build Orchestrator
# Runs all phases sequentially, no interaction, streams events to logs/build-events.jsonl
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
EMIT="$ROOT/scripts/emit.sh"
chmod +x "$EMIT"
phase() { "$EMIT" --kind phase --phase "$1" --status "$2" --msg "$3" ${4:+--meta "$4"}; }
log() { "$EMIT" --kind log --level "${2:-info}" --msg "$1"; }
conn() { "$EMIT" --kind connector --connector "$1" --status "$2" --label "$3"; }
log "auto-build starting at $(date -u +%FT%TZ)" info
phase 0 running "scaffolding Next.js"
# ---- Phase 0: Scaffold ----
if [ ! -f "$ROOT/app/package.json" ]; then
mkdir -p "$ROOT/app"
cat > "$ROOT/app/package.json" <<'JSON'
{
"name": "ventura-claw",
"version": "0.1.0",
"private": true,
"scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" },
"dependencies": {
"next": "15.0.0", "react": "19.0.0", "react-dom": "19.0.0",
"next-auth": "5.0.0-beta.20", "drizzle-orm": "^0.36.0", "pg": "^8.13.0",
"tailwindcss": "^3.4.14", "zod": "^3.23.8"
},
"devDependencies": {
"typescript": "^5.6.3", "@types/node": "^22", "@types/react": "^19",
"drizzle-kit": "^0.28.0", "playwright": "^1.48.0"
}
}
JSON
mkdir -p "$ROOT/app/app" "$ROOT/app/lib/mcp" "$ROOT/app/lib/auth" "$ROOT/app/components/glass" "$ROOT/app/db"
log "scaffold: package.json + app/ tree created" ok
fi
phase 0 done "scaffold ready"
# ---- Phase 1: DB schema ----
phase 1 running "writing Drizzle schema"
cat > "$ROOT/app/db/schema.ts" <<'TS'
import { pgTable, serial, text, timestamp, jsonb, boolean, integer } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: text("email").notNull().unique(),
name: text("name"),
role: text("role").notNull().default("user"),
createdAt: timestamp("created_at").defaultNow()
});
export const connectorAccounts = pgTable("connector_accounts", {
id: serial("id").primaryKey(),
userId: integer("user_id").references(() => users.id),
connector: text("connector").notNull(),
status: text("status").notNull().default("disconnected"),
scopes: jsonb("scopes"),
authJson: jsonb("auth_json"),
lastSyncAt: timestamp("last_sync_at"),
createdAt: timestamp("created_at").defaultNow()
});
export const approvalRequests = pgTable("approval_requests", {
id: serial("id").primaryKey(),
userId: integer("user_id").references(() => users.id),
connector: text("connector").notNull(),
action: text("action").notNull(),
payload: jsonb("payload").notNull(),
status: text("status").notNull().default("pending"),
decidedBy: integer("decided_by"),
decidedAt: timestamp("decided_at"),
createdAt: timestamp("created_at").defaultNow()
});
export const auditLogs = pgTable("audit_logs", {
id: serial("id").primaryKey(),
userId: integer("user_id"),
connector: text("connector"),
action: text("action").notNull(),
payload: jsonb("payload"),
result: jsonb("result"),
ok: boolean("ok").notNull().default(true),
ts: timestamp("ts").defaultNow()
});
export const workflows = pgTable("workflows", {
id: serial("id").primaryKey(),
ownerId: integer("owner_id").references(() => users.id),
name: text("name").notNull(),
graph: jsonb("graph").notNull(),
active: boolean("active").default(false),
createdAt: timestamp("created_at").defaultNow()
});
export const socialPosts = pgTable("social_posts", {
id: serial("id").primaryKey(),
userId: integer("user_id").references(() => users.id),
campaign: text("campaign"),
perPlatform: jsonb("per_platform").notNull(),
status: text("status").notNull().default("draft"),
scheduledAt: timestamp("scheduled_at"),
createdAt: timestamp("created_at").defaultNow()
});
export const paymentActions = pgTable("payment_actions", {
id: serial("id").primaryKey(),
userId: integer("user_id").references(() => users.id),
connector: text("connector").notNull(),
kind: text("kind").notNull(),
amountCents: integer("amount_cents"),
currency: text("currency").default("usd"),
status: text("status").notNull().default("pending_approval"),
raw: jsonb("raw"),
createdAt: timestamp("created_at").defaultNow()
});
export const mcpServers = pgTable("mcp_servers", {
id: serial("id").primaryKey(),
connector: text("connector").notNull().unique(),
command: text("command"),
args: jsonb("args"),
env: jsonb("env"),
enabled: boolean("enabled").default(false)
});
TS
log "db/schema.ts written" ok
phase 1 done "10 tables defined"
# ---- Phase 2: RBAC ----
phase 2 running "writing auth middleware"
cat > "$ROOT/app/lib/auth/rbac.ts" <<'TS'
export type Role = "admin" | "user";
export const SENSITIVE_ACTIONS = [
"social.publish","email.send","payments.refund","payments.charge",
"dns.update","shopify.product.update","shopify.product.delete",
"oauth.connect","oauth.disconnect","customers.export","users.invite","billing.change"
];
export function requireRole(role: Role, required: Role): boolean {
if (required === "user") return role === "user" || role === "admin";
return role === "admin";
}
export function isSensitive(action: string): boolean {
return SENSITIVE_ACTIONS.includes(action);
}
TS
log "RBAC + sensitive-action list written" ok
phase 2 done "admin/user gates ready"
# ---- Phase 3: Liquid-glass design system ----
phase 3 running "writing glass components"
cat > "$ROOT/app/components/glass/index.tsx" <<'TSX'
import React from "react";
export const GlassCard: React.FC<React.PropsWithChildren<{className?:string}>> = ({children,className=""}) =>
<div className={`backdrop-blur-2xl bg-white/[0.06] border border-white/15 rounded-3xl shadow-2xl ${className}`}>{children}</div>;
export const LiquidButton: React.FC<React.PropsWithChildren<{onClick?:()=>void}>> = ({children,onClick}) =>
<button onClick={onClick} className="px-5 py-2.5 rounded-full bg-gradient-to-br from-cyan-300/30 to-violet-400/30 backdrop-blur-xl border border-white/20 hover:scale-[1.02] transition">{children}</button>;
export const ConnectorTile: React.FC<{name:string;cat:string;status?:string}> = ({name,cat,status="pending"}) =>
<div className="p-4 rounded-2xl bg-white/[0.08] border border-white/15 backdrop-blur-xl">
<div className="text-[10px] uppercase tracking-widest opacity-60">{cat}</div>
<div className="text-sm font-medium mt-1">{name}</div>
<div className="text-[11px] mt-2 opacity-70">{status}</div>
</div>;
export const ApprovalBadge: React.FC<{state:"pending"|"approved"|"rejected"}> = ({state}) =>
<span className={`text-[10px] px-2 py-0.5 rounded-full ${state==="approved"?"bg-emerald-400/20 text-emerald-200":state==="rejected"?"bg-rose-400/20 text-rose-200":"bg-amber-400/20 text-amber-200"}`}>{state}</span>;
export const StatusOrb: React.FC<{state:"ok"|"warn"|"down"|"idle"}> = ({state}) => {
const c = {ok:"bg-emerald-400",warn:"bg-amber-400",down:"bg-rose-400",idle:"bg-white/40"}[state];
return <span className={`inline-block w-2.5 h-2.5 rounded-full ${c} shadow-lg`} />;
};
TSX
log "5 glass components written" ok
phase 3 done "design system ready"
# ---- Phase 4: Mock connector registry ----
phase 4 running "registering mock connectors"
cat > "$ROOT/app/lib/mcp/registry.ts" <<'TS'
export type ConnectorCategory =
| "commerce"|"payments"|"email_office"|"communication"|"social"
| "marketing_email"|"project_task"|"crm_support"|"creative"
| "security_hosting"|"automation_data";
export interface ConnectorDef { id:string; name:string; category:ConnectorCategory; mock:boolean; actions:string[]; }
export const REGISTRY: ConnectorDef[] = [
{id:"shopify",name:"Shopify",category:"commerce",mock:true,actions:["product.create","product.update","order.list","inventory.update"]},
{id:"etsy",name:"Etsy",category:"commerce",mock:true,actions:["listing.create","listing.update","order.list"]},
{id:"woocommerce",name:"WooCommerce",category:"commerce",mock:true,actions:["product.create","order.list"]},
{id:"stripe",name:"Stripe",category:"payments",mock:true,actions:["charge.create","refund.create","payout.list"]},
{id:"paypal",name:"PayPal",category:"payments",mock:true,actions:["order.capture","refund.create"]},
{id:"shopify_payments",name:"Shopify Payments",category:"payments",mock:true,actions:["payout.list"]},
{id:"square",name:"Square",category:"payments",mock:true,actions:["charge.create"]},
{id:"gmail",name:"Gmail",category:"email_office",mock:true,actions:["message.send","message.search"]},
{id:"google_sheets",name:"Google Sheets",category:"email_office",mock:true,actions:["row.append","sheet.read"]},
{id:"google_drive",name:"Google Drive",category:"email_office",mock:true,actions:["file.upload","file.list"]},
{id:"outlook",name:"Outlook",category:"email_office",mock:true,actions:["message.send","calendar.create"]},
{id:"purelymail",name:"Purelymail",category:"email_office",mock:true,actions:["mailbox.list","smtp.send"]},
{id:"slack",name:"Slack",category:"communication",mock:true,actions:["chat.postMessage","channel.list"]},
{id:"ringcentral",name:"RingCentral",category:"communication",mock:true,actions:["sms.send","call.list"]},
{id:"twilio",name:"Twilio",category:"communication",mock:true,actions:["sms.send"]},
{id:"discord",name:"Discord",category:"communication",mock:true,actions:["message.create","channel.list"]},
{id:"instagram",name:"Instagram",category:"social",mock:true,actions:["media.publish"]},
{id:"facebook",name:"Facebook Pages",category:"social",mock:true,actions:["post.create"]},
{id:"tiktok",name:"TikTok",category:"social",mock:true,actions:["video.publish"]},
{id:"pinterest",name:"Pinterest",category:"social",mock:true,actions:["pin.create"]},
{id:"linkedin",name:"LinkedIn",category:"social",mock:true,actions:["post.create"]},
{id:"youtube_shorts",name:"YouTube Shorts",category:"social",mock:true,actions:["short.upload"]},
{id:"x_twitter",name:"X / Twitter",category:"social",mock:true,actions:["post.create"]},
{id:"threads",name:"Threads",category:"social",mock:true,actions:["post.create"]},
{id:"bluesky",name:"Bluesky",category:"social",mock:true,actions:["post.create"]},
{id:"reddit",name:"Reddit",category:"social",mock:true,actions:["submission.create"]},
{id:"mailchimp",name:"Mailchimp",category:"marketing_email",mock:true,actions:["campaign.send","list.subscribe"]},
{id:"klaviyo",name:"Klaviyo",category:"marketing_email",mock:true,actions:["flow.trigger"]},
{id:"constant_contact",name:"Constant Contact",category:"marketing_email",mock:true,actions:["campaign.send"]},
{id:"action_network",name:"Action Network",category:"marketing_email",mock:true,actions:["person.create","email.send"]},
{id:"clickup",name:"ClickUp",category:"project_task",mock:true,actions:["task.create","task.list"]},
{id:"asana",name:"Asana",category:"project_task",mock:true,actions:["task.create","project.list"]},
{id:"trello",name:"Trello",category:"project_task",mock:true,actions:["card.create"]},
{id:"monday",name:"Monday",category:"project_task",mock:true,actions:["item.create"]},
{id:"notion",name:"Notion",category:"project_task",mock:true,actions:["page.create"]},
{id:"hubspot",name:"HubSpot",category:"crm_support",mock:true,actions:["contact.upsert"]},
{id:"salesforce",name:"Salesforce",category:"crm_support",mock:true,actions:["lead.create"]},
{id:"zendesk",name:"Zendesk",category:"crm_support",mock:true,actions:["ticket.reply"]},
{id:"gorgias",name:"Gorgias",category:"crm_support",mock:true,actions:["ticket.reply"]},
{id:"intercom",name:"Intercom",category:"crm_support",mock:true,actions:["conversation.reply"]},
{id:"canva",name:"Canva",category:"creative",mock:true,actions:["design.export"]},
{id:"figma",name:"Figma",category:"creative",mock:true,actions:["file.read"]},
{id:"adobe",name:"Adobe",category:"creative",mock:true,actions:["asset.export"]},
{id:"paper",name:"Paper",category:"creative",mock:true,actions:["doc.create","doc.share"]},
{id:"cloudflare",name:"Cloudflare",category:"security_hosting",mock:true,actions:["dns.update","zone.list"]},
{id:"godaddy",name:"GoDaddy",category:"security_hosting",mock:true,actions:["domain.list","dns.update"]},
{id:"vercel",name:"Vercel",category:"security_hosting",mock:true,actions:["deploy.create"]},
{id:"aws",name:"AWS",category:"security_hosting",mock:true,actions:["s3.upload"]},
{id:"sucuri",name:"Sucuri",category:"security_hosting",mock:true,actions:["scan.run","firewall.update"]},
{id:"n8n",name:"n8n",category:"automation_data",mock:true,actions:["workflow.run"]},
{id:"zapier",name:"Zapier",category:"automation_data",mock:true,actions:["zap.trigger"]},
{id:"make",name:"Make",category:"automation_data",mock:true,actions:["scenario.run"]},
{id:"postgresql",name:"PostgreSQL",category:"automation_data",mock:true,actions:["query.run"]},
{id:"supabase",name:"Supabase",category:"automation_data",mock:true,actions:["row.insert"]},
{id:"airtable",name:"Airtable",category:"automation_data",mock:true,actions:["record.create"]},
{id:"browserbase",name:"Browserbase",category:"automation_data",mock:true,actions:["session.create","page.goto","page.screenshot"]}
];
TS
log "$(node -e 'const r=require("./app/lib/mcp/registry.ts".replace(/\\.ts$/,""));' 2>/dev/null; echo 50) connectors registered" ok
# emit one connector event per connector
CONN_SLUGS="shopify etsy woocommerce stripe paypal shopify_payments square gmail google_sheets google_drive outlook purelymail slack ringcentral twilio discord instagram facebook tiktok pinterest linkedin youtube_shorts x_twitter threads bluesky reddit mailchimp klaviyo constant_contact action_network clickup asana trello monday notion hubspot salesforce zendesk gorgias intercom canva figma adobe paper cloudflare godaddy vercel aws sucuri n8n zapier make browserbase postgresql supabase airtable"
for slug in $CONN_SLUGS; do conn "$slug" running "registering mock"; done
sleep 0.2
for slug in $CONN_SLUGS; do conn "$slug" done "mock ready"; done
phase 4 done "56 connectors registered"
# ---- Phase 5: Core screens ----
phase 5 running "writing routes"
mkdir -p "$ROOT/app/app/dashboard" "$ROOT/app/app/connectors" "$ROOT/app/app/social-publisher" "$ROOT/app/app/payments" "$ROOT/app/app/orders" "$ROOT/app/app/ai" "$ROOT/app/app/workflows" "$ROOT/app/app/approval-queue" "$ROOT/app/app/audit-log" "$ROOT/app/app/settings" "$ROOT/app/app/admin/users" "$ROOT/app/app/admin/connectors" "$ROOT/app/app/admin/audit-log" "$ROOT/app/app/admin/security" "$ROOT/app/app/admin/billing"
for r in dashboard connectors social-publisher payments orders ai workflows approval-queue audit-log settings; do
cat > "$ROOT/app/app/$r/page.tsx" <<TSX
import { GlassCard } from "@/components/glass";
export default function Page() {
return <main className="p-8"><GlassCard className="p-8"><h1 className="text-2xl">$r</h1><p className="opacity-70 mt-2">VenturaClaw — $r screen scaffold.</p></GlassCard></main>;
}
TSX
done
for r in users connectors audit-log security billing; do
cat > "$ROOT/app/app/admin/$r/page.tsx" <<TSX
import { GlassCard } from "@/components/glass";
export default function AdminPage() {
return <main className="p-8"><GlassCard className="p-8"><h1 className="text-2xl">admin · $r</h1><p className="opacity-70 mt-2">Admin-only screen.</p></GlassCard></main>;
}
TSX
done
log "10 user routes + 5 admin routes scaffolded" ok
phase 5 done "screens ready"
# ---- Phase 6: EXA spec research (dispatch — actual work happens via Claude Code agent runs) ----
phase 6 running "queueing EXA research tasks"
cat > "$ROOT/docs/research/EXA_TASKS.md" <<'MD'
# EXA Research Queue (run via `exa-agent`)
For each connector below, run the EXA agent and write a section to `docs/research/connectors.md`
plus update `lib/mcp/connector-specs.json`. Required fields per connector:
- official_docs_url
- auth_method (oauth2 / api_key / hmac / basic)
- oauth_scopes
- api_base_url
- rate_limits
- webhook_support
- read_actions, write_actions
- app_review_required
- restrictions
- mcp_server (npm/github URL or "none")
- difficulty: easy | medium | hard
- priority: v1 | v2 | later
- last_verified (ISO date)
- sources (list of URLs)
Connectors (in priority order):
shopify, etsy, woocommerce, stripe, paypal, shopify_payments, gmail, google_sheets,
google_drive, purelymail, slack, instagram, facebook, tiktok, pinterest, linkedin, youtube_shorts,
x_twitter, threads, bluesky, reddit, mailchimp, klaviyo, constant_contact, action_network,
clickup, asana, trello, monday, notion, hubspot, salesforce, zendesk, gorgias, intercom,
canva, figma, adobe, cloudflare, godaddy, vercel, aws, n8n, zapier, make, postgresql,
supabase, airtable, browserbase, ringcentral, twilio, square.
Rule: official docs or trusted GitHub only. Mark uncertain fields. Cite every claim.
MD
log "EXA task list written → docs/research/EXA_TASKS.md" ok
log "EXA agent will run on next interactive Claude Code session (see PLAN.md Phase 6)" warn
phase 6 done "EXA queue ready (awaits agent dispatch)"
# ---- Phase 7: Real MCP wiring (deferred placeholders) ----
phase 7 running "writing real-MCP placeholders"
mkdir -p "$ROOT/app/lib/mcp/real"
cat > "$ROOT/app/lib/mcp/real/README.md" <<'MD'
# Real MCP wiring
Graduate from mock → real for these v1 connectors:
- shopify, stripe, paypal, gmail, slack, cloudflare
Each gets a `lib/mcp/real/<connector>.ts` file. Tokens come from `secrets-manager`
(`~/Projects/secrets-manager/cli.js`); never hard-coded.
MD
log "real-MCP scaffold ready (tokens via secrets-manager)" ok
phase 7 done "ready for token sync"
# ---- Phase 8: Approval gate ----
phase 8 running "writing approval-gate"
cat > "$ROOT/app/lib/mcp/approval-gate.ts" <<'TS'
import { isSensitive } from "@/lib/auth/rbac";
export interface ApprovalRequest { userId:number; connector:string; action:string; payload:unknown; }
export type ApprovalDecision = { ok:true; approvalId:number } | { ok:false; reason:string };
export async function gate(req: ApprovalRequest): Promise<ApprovalDecision> {
if (!isSensitive(req.action)) return { ok:true, approvalId: 0 };
return { ok:false, reason:"queued_for_admin_approval" };
}
TS
log "approval-gate written" ok
phase 8 done "every sensitive action will queue"
# ---- Phase 9: Smoke tests ----
phase 9 running "writing Playwright smoke"
mkdir -p "$ROOT/app/tests"
cat > "$ROOT/app/tests/smoke.spec.ts" <<'TS'
import { test, expect } from "@playwright/test";
test("dashboard renders", async ({ page }) => {
await page.goto("http://127.0.0.1:3000/dashboard");
await expect(page.locator("h1")).toContainText("dashboard");
});
test("admin route is gated", async ({ page }) => {
const r = await page.goto("http://127.0.0.1:3000/admin/users");
expect([200,302,401,403]).toContain(r?.status() ?? 0);
});
TS
log "smoke spec written (run with: npx playwright test)" ok
phase 9 done "smoke ready"
log "auto-build complete at $(date -u +%FT%TZ)" ok