improved a lot of things
This commit is contained in:
@@ -57,7 +57,67 @@
|
||||
"Bash(echo \"BUILD_EXIT:$?\")",
|
||||
"Bash(node /Users/USER/Desktop/originmain/node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/bin/next build)",
|
||||
"Bash(pkill -f \"next build\")",
|
||||
"Bash(pnpm --filter @originmain/app build)"
|
||||
"Bash(pnpm --filter @originmain/app build)",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/linear.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/slack.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/cursor.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/anthropic.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/typescript.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/nextdotjs.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/react.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/slack.svg\" -o /dev/null -w \"%{http_code}\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/visualstudiocode.svg\" -o /dev/null -w \"%{http_code}\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/_data/simple-icons.json\")",
|
||||
"Bash(python3 -c ' *)",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/simple-icons.json\")",
|
||||
"Bash(curl -s \"https://api.github.com/repos/simple-icons/simple-icons/contents/icons\")",
|
||||
"Bash(curl -sI \"https://cdn.simpleicons.org/slack\")",
|
||||
"Bash(curl -sI \"https://cdn.simpleicons.org/visualstudiocode\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/master/icons/slack.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/master/icons/visualstudiocode.svg\")",
|
||||
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(d.get\\('tag_name',''\\)\\)\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/16.18.0/icons/slack.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/16.18.0/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/16.18.0/_data/simple-icons.json\")",
|
||||
"Bash(curl -s \"https://registry.npmjs.org/simple-icons/latest\")",
|
||||
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(d.get\\('version',''\\)\\)\")",
|
||||
"Bash(curl -s \"https://unpkg.com/simple-icons/icons/slack.svg\")",
|
||||
"Bash(curl -s \"https://unpkg.com/simple-icons/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons/icons/slack.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons/simple-icons.json\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@16.18.0/\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@16.18.0/?meta\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@13.22.0/icons/slack.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@13.22.0/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@14.0.0/icons/slack.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@14.0.0/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@14.0.0/?meta\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@9.0.0/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@9.0.0/simple-icons.json\")",
|
||||
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/14.0.0/_data/simple-icons.json\" -o /tmp/si14.json)",
|
||||
"Read(//tmp/**)",
|
||||
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/9.0.0/_data/simple-icons.json\" -o /tmp/si9.json)",
|
||||
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/_data/simple-icons.json\" -o /tmp/si_dev.json)",
|
||||
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/cursor.svg\")",
|
||||
"Bash(curl -s \"https://api.github.com/repos/simple-icons/simple-icons/git/trees/develop?recursive=0\")",
|
||||
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/data/simple-icons.json\" -o /tmp/si_develop.json)",
|
||||
"Bash(pnpm --filter @originmain/agent-bridge exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/integrations exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/multiplayer exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/platform exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/ai-layer exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/design-language exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/origin-graph exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter originmain-app exec tsc --noEmit)",
|
||||
"WebFetch(domain:raw.githubusercontent.com)",
|
||||
"WebFetch(domain:cdn.simpleicons.org)",
|
||||
"WebFetch(domain:brandfetch.com)",
|
||||
"WebFetch(domain:simpleicons.org)",
|
||||
"WebFetch(domain:www.svgrepo.com)",
|
||||
"WebFetch(domain:worldvectorlogo.com)",
|
||||
"Bash(pnpm --filter @originmain/app exec tsc --noEmit)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2135,9 +2135,8 @@ footer {
|
||||
<div class="int-card reveal"
|
||||
style="--glow:#ffffff;--glow-dim:rgba(255,255,255,.07);--glow-soft:rgba(255,255,255,.03);--glow-ambient:rgba(255,255,255,.02);--ico-bg:rgba(255,255,255,.06);--ico-bd:rgba(255,255,255,.1)">
|
||||
<div class="int-ico">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="rgba(255,255,255,.5)" stroke-width="1.2"/>
|
||||
<path d="M8 8v8l8-8v8" stroke="rgba(255,255,255,.85)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none">
|
||||
<path d="M5 4v16l14-16v16" stroke="rgba(255,255,255,.88)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# ── Clerk (authentication) ────────────────────────────────────────────────────
|
||||
# https://dashboard.clerk.com → API Keys
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
|
||||
CLERK_SECRET_KEY=sk_test_...
|
||||
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
||||
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
||||
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/app
|
||||
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/app
|
||||
|
||||
# ── Supabase ──────────────────────────────────────────────────────────────────
|
||||
# https://supabase.com/dashboard → Project Settings → API
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://<project-ref>.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
|
||||
SUPABASE_SERVICE_ROLE_KEY=eyJ...
|
||||
|
||||
# ── Anthropic ─────────────────────────────────────────────────────────────────
|
||||
# https://console.anthropic.com/keys
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
|
||||
# ── Agent Bridge ─────────────────────────────────────────────────────────────
|
||||
# Random secret used to sign workspace tokens issued to Cursor / Claude Code.
|
||||
# Generate with: openssl rand -hex 32
|
||||
AGENT_BRIDGE_SECRET=
|
||||
|
||||
# ── Webhook signing secrets (optional — skip verification in dev if absent) ──
|
||||
# Linear: Settings → API → Webhooks → signing secret
|
||||
LINEAR_WEBHOOK_SECRET=
|
||||
# Slack: App → Basic Information → Signing Secret
|
||||
SLACK_SIGNING_SECRET=
|
||||
# GitHub: Repository → Settings → Webhooks → secret
|
||||
GITHUB_WEBHOOK_SECRET=
|
||||
# Intercom: Developer Hub → Webhooks → client secret
|
||||
INTERCOM_WEBHOOK_SECRET=
|
||||
@@ -1,7 +1,19 @@
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: ['@originmain/ui', '@originmain/diff-engine', '@fluentui/react-components'],
|
||||
transpilePackages: [
|
||||
'@originmain/agent-bridge',
|
||||
'@originmain/ai-layer',
|
||||
'@originmain/design-language',
|
||||
'@originmain/diff-engine',
|
||||
'@originmain/integrations',
|
||||
'@originmain/multiplayer',
|
||||
'@originmain/origin-graph',
|
||||
'@originmain/platform',
|
||||
'@originmain/renderer',
|
||||
'@originmain/ui',
|
||||
'@fluentui/react-components',
|
||||
],
|
||||
experimental: {
|
||||
optimizePackageImports: ['@fluentui/react-components'],
|
||||
},
|
||||
|
||||
@@ -9,15 +9,23 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.56.0",
|
||||
"@clerk/nextjs": "^7.2.7",
|
||||
"@fluentui/react-components": "^9.54.0",
|
||||
"@fluentui/react-icons": "^2.0.0",
|
||||
"@originmain/agent-bridge": "workspace:*",
|
||||
"@originmain/ai-layer": "workspace:*",
|
||||
"@originmain/design-language": "workspace:*",
|
||||
"@originmain/diff-engine": "workspace:*",
|
||||
"@originmain/integrations": "workspace:*",
|
||||
"@originmain/multiplayer": "workspace:*",
|
||||
"@originmain/origin-graph": "workspace:*",
|
||||
"@originmain/platform": "workspace:*",
|
||||
"@originmain/renderer": "workspace:*",
|
||||
"@originmain/ui": "workspace:*",
|
||||
"@pierre/diffs": "^1.1.19",
|
||||
"@pierre/trees": "1.0.0-beta.3",
|
||||
"@supabase/supabase-js": "^2.0.0",
|
||||
"@tanstack/react-query": "^5.62.0",
|
||||
"next": "^15.1.0",
|
||||
"react": "^19.0.0",
|
||||
|
||||
@@ -2022,9 +2022,9 @@ footer {
|
||||
<div class="int-card reveal"
|
||||
style="--glow:#5e6ad2;--glow-dim:rgba(94,106,210,.14);--glow-soft:rgba(94,106,210,.06);--glow-ambient:rgba(94,106,210,.04);--ico-bg:rgba(94,106,210,.12);--ico-bd:rgba(94,106,210,.22)">
|
||||
<div class="int-ico">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
|
||||
<path d="M4 20L20 4" stroke="#5e6ad2" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M20 4v10M20 4H10" stroke="#5e6ad2" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<!-- Linear official mark — fetched from simple-icons/simple-icons/icons/linear.svg -->
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="#5e6ad2">
|
||||
<path d="M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
@@ -2041,11 +2041,12 @@ footer {
|
||||
<div class="int-card reveal"
|
||||
style="--glow:#2eb67d;--glow-dim:rgba(46,182,125,.12);--glow-soft:rgba(46,182,125,.05);--glow-ambient:rgba(46,182,125,.04);--ico-bg:rgba(46,182,125,.1);--ico-bd:rgba(46,182,125,.18)">
|
||||
<div class="int-ico">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
|
||||
<path d="M9.07 3a2.07 2.07 0 00-2.07 2.07v5.79M7 10.86A2.07 2.07 0 113 10.86M16.93 3a2.07 2.07 0 012.07 2.07v5.79" stroke="#2eb67d" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<path d="M19 10.86a2.07 2.07 0 110 4.14M9.07 21a2.07 2.07 0 002.07-2.07v-5.79M11.14 13.14a2.07 2.07 0 110-4.14" stroke="#e01d5a" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<path d="M7 13.14h5.79M5 15.21a2.07 2.07 0 104.14 0" stroke="#ecb22e" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<path d="M13.14 15a2.07 2.07 0 104.14 0v-2.07M13.14 17.07a2.07 2.07 0 100 4.14" stroke="#36c5f0" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<!-- Slack official mark — paths from gilbarbara/logos slack-icon.svg -->
|
||||
<svg viewBox="0 0 256 256" width="20" height="20" fill="none">
|
||||
<path d="M53.841 161.321C53.841 176.152 41.854 188.14 27.022 188.14 12.19 188.14.203 176.152.203 161.321c0-14.832 11.987-26.82 26.819-26.82h26.82v26.82zm13.41 0c0-14.832 11.987-26.82 26.819-26.82 14.832 0 26.819 11.988 26.819 26.82v67.047c0 14.832-11.987 26.82-26.819 26.82-14.832 0-26.819-11.988-26.819-26.82V161.32z" fill="#E01E5A"/>
|
||||
<path d="M94.07 53.638C79.238 53.638 67.251 41.651 67.251 26.819 67.251 11.987 79.238 0 94.07 0c14.832 0 26.819 11.987 26.819 26.819v26.82H94.07zm0 13.613c14.832 0 26.819 11.987 26.819 26.819 0 14.832-11.987 26.819-26.819 26.819H26.819C11.987 120.889 0 108.902 0 94.07c0-14.832 11.987-26.819 26.819-26.819H94.07z" fill="#36C5F0"/>
|
||||
<path d="M201.549 94.07c0-14.832 11.987-26.819 26.819-26.819C243.2 67.251 255.187 79.238 255.187 94.07c0 14.832-11.987 26.819-26.819 26.819h-26.819V94.07zm-13.41 0c0 14.832-11.987 26.819-26.819 26.819-14.832 0-26.819-11.987-26.819-26.819V26.819C134.501 11.987 146.488 0 161.32 0c14.832 0 26.819 11.987 26.819 26.819V94.07z" fill="#2EB67D"/>
|
||||
<path d="M161.321 201.549c14.832 0 26.819 11.987 26.819 26.819 0 14.832-11.987 26.819-26.819 26.819-14.832 0-26.819-11.987-26.819-26.819v-26.819h26.819zm0-13.41c-14.832 0-26.819-11.987-26.819-26.819 0-14.832 11.987-26.819 26.819-26.819h67.25c14.832 0 26.819 11.987 26.819 26.819 0 14.832-11.987 26.819-26.819 26.819h-67.25z" fill="#ECB22E"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
@@ -2062,8 +2063,9 @@ footer {
|
||||
<div class="int-card w2 reveal"
|
||||
style="--glow:#d4d4d4;--glow-dim:rgba(212,212,212,.09);--glow-soft:rgba(212,212,212,.04);--glow-ambient:rgba(212,212,212,.02);--ico-bg:rgba(255,255,255,.06);--ico-bd:rgba(255,255,255,.1)">
|
||||
<div class="int-ico">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
|
||||
<path d="M5 3l14 9.5-8.5 1.5L8 21 5 3z" fill="rgba(255,255,255,.8)" stroke="rgba(255,255,255,.5)" stroke-width="1" stroke-linejoin="round"/>
|
||||
<!-- Cursor official mark — fetched from simple-icons/simple-icons/icons/cursor.svg -->
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="rgba(255,255,255,.88)">
|
||||
<path d="M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
@@ -2080,8 +2082,9 @@ footer {
|
||||
<div class="int-card reveal"
|
||||
style="--glow:#007acc;--glow-dim:rgba(0,122,204,.14);--glow-soft:rgba(0,122,204,.06);--glow-ambient:rgba(0,122,204,.04);--ico-bg:rgba(0,122,204,.12);--ico-bd:rgba(0,122,204,.22)">
|
||||
<div class="int-ico">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
|
||||
<path d="M17.5 2L3 9.5l4.5 3.5L17.5 6v12L7.5 12 3 15.5 17.5 22l3-1.5V3.5L17.5 2z" fill="#007acc"/>
|
||||
<!-- VS Code official mark: folded-ribbon square — simpleicons.org/visualstudiocode -->
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="#007acc">
|
||||
<path d="M23.15 2.587 18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
@@ -2098,8 +2101,9 @@ footer {
|
||||
<div class="int-card reveal"
|
||||
style="--glow:#f59e0b;--glow-dim:rgba(245,158,11,.13);--glow-soft:rgba(245,158,11,.05);--glow-ambient:rgba(245,158,11,.04);--ico-bg:rgba(245,158,11,.1);--ico-bd:rgba(245,158,11,.2)">
|
||||
<div class="int-ico">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
|
||||
<path d="M12 3L5 19h4.5l2.5-5 2.5 5H19L12 3z" fill="#f59e0b"/>
|
||||
<!-- Claude official mark — fetched from simple-icons/simple-icons/icons/claude.svg -->
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="#f59e0b">
|
||||
<path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
@@ -2116,9 +2120,13 @@ footer {
|
||||
<div class="int-card reveal"
|
||||
style="--glow:#3b82f6;--glow-dim:rgba(59,130,246,.14);--glow-soft:rgba(59,130,246,.06);--glow-ambient:rgba(59,130,246,.04);--ico-bg:#1e3a5f;--ico-bd:rgba(59,130,246,.3)">
|
||||
<div class="int-ico" style="background:#1d4ed8;border-color:rgba(59,130,246,.4)">
|
||||
<!-- TypeScript official mark: blue square with correct TS letterforms -->
|
||||
<svg viewBox="0 0 18 18" width="18" height="18">
|
||||
<rect width="18" height="18" rx="2" fill="#3178c6"/>
|
||||
<path d="M3 10.5h4.5M5.25 8v5M10 10.5c.5-1.5 3.5-1.5 3.5.5s-3.5 1-3.5 2.5 3 2 3.5.5" stroke="white" stroke-width="1.4" stroke-linecap="round"/>
|
||||
<!-- T: crossbar at TOP, stem descends from centre -->
|
||||
<path d="M2 6.5h6M5 6.5v7" stroke="white" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
<!-- S: top curve right, mid narrows, bottom curve left -->
|
||||
<path d="M10 13.5c.5.8 1.4 1.2 2.6 1.2 1.5 0 2.8-.8 2.8-2.2 0-1.1-.7-1.8-2.1-2.3-1.1-.4-1.8-.8-1.8-1.6 0-.7.6-1.3 1.7-1.3.9 0 1.6.3 2.1 1" stroke="white" stroke-width="1.5" stroke-linecap="round" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
@@ -2135,9 +2143,8 @@ footer {
|
||||
<div class="int-card reveal"
|
||||
style="--glow:#ffffff;--glow-dim:rgba(255,255,255,.07);--glow-soft:rgba(255,255,255,.03);--glow-ambient:rgba(255,255,255,.02);--ico-bg:rgba(255,255,255,.06);--ico-bd:rgba(255,255,255,.1)">
|
||||
<div class="int-ico">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="rgba(255,255,255,.5)" stroke-width="1.2"/>
|
||||
<path d="M8 8v8l8-8v8" stroke="rgba(255,255,255,.85)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none">
|
||||
<path d="M5 4v16l14-16v16" stroke="rgba(255,255,255,.88)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// POST /api/agent-bridge
|
||||
// JSON-RPC 2.0 endpoint consumed by the Cursor / Claude Code MCP adapters.
|
||||
// Authentication: Bearer <workspace_token> (HMAC-SHA256, issued by issueWorkspaceToken).
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { verifyWorkspaceToken, TOOL_MAP } from '@originmain/agent-bridge';
|
||||
import type { JsonRpcRequest, ToolContext } from '@originmain/agent-bridge';
|
||||
import {
|
||||
getDiffsByStatus,
|
||||
getDiff,
|
||||
getArtboard,
|
||||
getActiveDesignLanguageFile,
|
||||
updateDiffStatus,
|
||||
} from '@originmain/origin-graph';
|
||||
import { AIGateway, answerAgentQuestion } from '@originmain/ai-layer';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
// ── Auth ────────────────────────────────────────────────────────────────────
|
||||
const authHeader = req.headers.get('authorization') ?? '';
|
||||
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ jsonrpc: '2.0', id: null, error: { code: -32001, message: 'Missing Bearer token' } },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceToken = verifyWorkspaceToken(token);
|
||||
if (!workspaceToken) {
|
||||
return NextResponse.json(
|
||||
{ jsonrpc: '2.0', id: null, error: { code: -32001, message: 'Invalid or expired token' } },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
// ── Parse JSON-RPC body ─────────────────────────────────────────────────────
|
||||
let body: JsonRpcRequest;
|
||||
try {
|
||||
body = (await req.json()) as JsonRpcRequest;
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// tools/list — meta-endpoint; return available tools without executing one.
|
||||
if (body.method === 'tools/list') {
|
||||
const tools = [...TOOL_MAP.values()].map(t => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
}));
|
||||
return NextResponse.json({ jsonrpc: '2.0', id: body.id, result: { tools } });
|
||||
}
|
||||
|
||||
// ── Dispatch ────────────────────────────────────────────────────────────────
|
||||
const tool = TOOL_MAP.get(body.method);
|
||||
if (!tool) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: '2.0',
|
||||
id: body.id,
|
||||
error: { code: -32601, message: `Method not found: ${body.method}` },
|
||||
});
|
||||
}
|
||||
|
||||
const db = serverClient();
|
||||
const { workspaceId } = workspaceToken;
|
||||
|
||||
const ctx: ToolContext = {
|
||||
workspaceId,
|
||||
db: {
|
||||
getDiffsByStatus: (wsId, status) => getDiffsByStatus(db, wsId, status),
|
||||
getDiff: (id) => getDiff(db, id),
|
||||
getArtboard: (id) => getArtboard(db, id),
|
||||
getDesignLanguageFile: (wsId) => getActiveDesignLanguageFile(db, wsId),
|
||||
updateDiffStatus: (id, status, notes) =>
|
||||
updateDiffStatus(db, id, status, notes).then(() => undefined),
|
||||
},
|
||||
ai: {
|
||||
answerAgentQuestion: (diffId: string, question: string, artboardContext: unknown) =>
|
||||
answerAgentQuestion(new AIGateway(), {
|
||||
diffId,
|
||||
question,
|
||||
artboardContextJson: JSON.stringify(artboardContext),
|
||||
}).then(r => r.answer),
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await tool.execute(body.params ?? {}, ctx);
|
||||
return NextResponse.json({ jsonrpc: '2.0', id: body.id, result });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Internal error';
|
||||
return NextResponse.json({
|
||||
jsonrpc: '2.0',
|
||||
id: body.id,
|
||||
error: { code: -32603, message },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// POST /api/ai/completion-zone
|
||||
// Fills a design completion zone given a component tree and user intent.
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { AIGateway, fillCompletionZone } from '@originmain/ai-layer';
|
||||
import type { CompletionZoneInput } from '@originmain/ai-layer';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = (await req.json()) as CompletionZoneInput;
|
||||
|
||||
try {
|
||||
const gateway = new AIGateway();
|
||||
const result = await fillCompletionZone(gateway, body);
|
||||
return NextResponse.json({ proposedTree: result.proposedTree, description: result.description });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'AI error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// POST /api/ai/drift-report
|
||||
// Analyzes a screenshot against the active Design Language File for drift violations.
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { AIGateway, generateDriftReport } from '@originmain/ai-layer';
|
||||
import type { DriftReportInput } from '@originmain/ai-layer';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = (await req.json()) as DriftReportInput;
|
||||
|
||||
try {
|
||||
const gateway = new AIGateway();
|
||||
const result = await generateDriftReport(gateway, body);
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'AI error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// POST /api/ai/query
|
||||
// Cross-artboard search: finds artboards matching a natural language query.
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { AIGateway, queryCrossArtboard } from '@originmain/ai-layer';
|
||||
import type { ArtboardQueryInput } from '@originmain/ai-layer';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = (await req.json()) as ArtboardQueryInput;
|
||||
|
||||
try {
|
||||
const gateway = new AIGateway();
|
||||
const result = await queryCrossArtboard(gateway, body);
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'AI error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// GET /api/artboards/:id → fetch single artboard
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { getArtboard } from '@originmain/origin-graph';
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const artboard = await getArtboard(db, id);
|
||||
return NextResponse.json(artboard);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
const status = message.includes('not found') || message.includes('0 rows') ? 404 : 500;
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// GET /api/artboards?workspaceId=<uuid> → list artboards for workspace
|
||||
// POST /api/artboards → create artboard
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { getArtboards, createArtboard } from '@originmain/origin-graph';
|
||||
import type { InsertArtboard } from '@originmain/origin-graph';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const workspaceId = req.nextUrl.searchParams.get('workspaceId');
|
||||
if (!workspaceId) return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const artboards = await getArtboards(db, workspaceId);
|
||||
return NextResponse.json(artboards);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = (await req.json()) as InsertArtboard;
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const artboard = await createArtboard(db, body);
|
||||
return NextResponse.json(artboard, { status: 201 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// GET /api/design-language?workspaceId=<uuid> → active design language file
|
||||
// POST /api/design-language → upsert (new version)
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { getActiveDesignLanguageFile } from '@originmain/origin-graph';
|
||||
import type { InsertDesignLanguageFile } from '@originmain/origin-graph';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const workspaceId = req.nextUrl.searchParams.get('workspaceId');
|
||||
if (!workspaceId) return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const file = await getActiveDesignLanguageFile(db, workspaceId);
|
||||
if (!file) return NextResponse.json(null, { status: 204 });
|
||||
return NextResponse.json(file);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = (await req.json()) as InsertDesignLanguageFile;
|
||||
const db = serverClient();
|
||||
|
||||
// Compute the next version: max(existing) + 1.
|
||||
const existing = await getActiveDesignLanguageFile(db, body.workspace_id);
|
||||
const version = existing ? existing.version + 1 : 1;
|
||||
|
||||
const { data, error } = await db
|
||||
.from('design_language_files')
|
||||
.insert({ ...body, version })
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json(data, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// GET /api/diffs/:id → fetch single diff
|
||||
// PATCH /api/diffs/:id → update status + notes
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { getDiff, updateDiffStatus } from '@originmain/origin-graph';
|
||||
import type { DiffStatus } from '@originmain/origin-graph';
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const diff = await getDiff(db, id);
|
||||
return NextResponse.json(diff);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
const status = message.includes('0 rows') ? 404 : 500;
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const body = (await req.json()) as { status: DiffStatus; notes?: string };
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const updated = await updateDiffStatus(db, id, body.status, body.notes);
|
||||
return NextResponse.json(updated);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// GET /api/diffs?artboardId=<uuid> → list diffs for an artboard
|
||||
// POST /api/diffs → create draft diff
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { getDiffs, createDiff } from '@originmain/origin-graph';
|
||||
import type { InsertIntentDiff } from '@originmain/origin-graph';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const artboardId = req.nextUrl.searchParams.get('artboardId');
|
||||
if (!artboardId) return NextResponse.json({ error: 'artboardId is required' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const diffs = await getDiffs(db, artboardId);
|
||||
return NextResponse.json(diffs);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = (await req.json()) as Omit<InsertIntentDiff, 'author_id'>;
|
||||
const insert: InsertIntentDiff = { ...body, author_id: userId };
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const diff = await createDiff(db, insert);
|
||||
return NextResponse.json(diff, { status: 201 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// POST /api/webhooks/:provider
|
||||
// Ingests webhook events from Linear, Slack, GitHub, and Intercom.
|
||||
// workspaceId must be passed as a query param: ?workspace=<uuid>
|
||||
// Signature verification is skipped gracefully when the secret env var is absent
|
||||
// (useful for local development), but enforced in production.
|
||||
|
||||
import { createHmac, timingSafeEqual } from 'node:crypto';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
linearIngester,
|
||||
slackIngester,
|
||||
githubIngester,
|
||||
intercomIngester,
|
||||
} from '@originmain/integrations';
|
||||
import type { OriginIngester } from '@originmain/integrations';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { createOrigin, createArtboard } from '@originmain/origin-graph';
|
||||
|
||||
// ── Signature helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
function hmac(algo: 'sha256' | 'sha1', secret: string, body: Buffer): string {
|
||||
return createHmac(algo, secret).update(body).digest('hex');
|
||||
}
|
||||
|
||||
function safeEqual(a: string, b: string): boolean {
|
||||
try {
|
||||
const ab = Buffer.from(a.padEnd(b.length, '\0'));
|
||||
const bb = Buffer.from(b.padEnd(a.length, '\0'));
|
||||
return timingSafeEqual(ab, bb) && a.length === b.length;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function verifyLinear(rawBody: Buffer, headers: Headers): boolean {
|
||||
const secret = process.env.LINEAR_WEBHOOK_SECRET;
|
||||
if (!secret) return true; // skip verification in dev
|
||||
const sig = headers.get('x-linear-signature') ?? '';
|
||||
return safeEqual(hmac('sha256', secret, rawBody), sig);
|
||||
}
|
||||
|
||||
function verifySlack(rawBody: Buffer, headers: Headers): boolean {
|
||||
const secret = process.env.SLACK_SIGNING_SECRET;
|
||||
if (!secret) return true;
|
||||
const ts = headers.get('x-slack-request-timestamp') ?? '';
|
||||
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
|
||||
const baseString = `v0:${ts}:${rawBody.toString()}`;
|
||||
const expected = `v0=${hmac('sha256', secret, Buffer.from(baseString))}`;
|
||||
const provided = headers.get('x-slack-signature') ?? '';
|
||||
return safeEqual(expected, provided);
|
||||
}
|
||||
|
||||
function verifyGitHub(rawBody: Buffer, headers: Headers): boolean {
|
||||
const secret = process.env.GITHUB_WEBHOOK_SECRET;
|
||||
if (!secret) return true;
|
||||
const expected = `sha256=${hmac('sha256', secret, rawBody)}`;
|
||||
const provided = headers.get('x-hub-signature-256') ?? '';
|
||||
return safeEqual(expected, provided);
|
||||
}
|
||||
|
||||
function verifyIntercom(rawBody: Buffer, headers: Headers): boolean {
|
||||
const secret = process.env.INTERCOM_WEBHOOK_SECRET;
|
||||
if (!secret) return true;
|
||||
const expected = `sha1=${hmac('sha1', secret, rawBody)}`;
|
||||
const provided = headers.get('x-hub-signature') ?? '';
|
||||
return safeEqual(expected, provided);
|
||||
}
|
||||
|
||||
// ── Route config ──────────────────────────────────────────────────────────────
|
||||
|
||||
const INGESTER_MAP: Record<
|
||||
string,
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ingester: OriginIngester<any>;
|
||||
verify: (body: Buffer, headers: Headers) => boolean;
|
||||
}
|
||||
> = {
|
||||
linear: { ingester: linearIngester, verify: verifyLinear },
|
||||
slack: { ingester: slackIngester, verify: verifySlack },
|
||||
github: { ingester: githubIngester, verify: verifyGitHub },
|
||||
intercom: { ingester: intercomIngester, verify: verifyIntercom },
|
||||
};
|
||||
|
||||
// ── Handler ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ provider: string }> },
|
||||
) {
|
||||
const { provider } = await params;
|
||||
const entry = INGESTER_MAP[provider];
|
||||
if (!entry) {
|
||||
return NextResponse.json({ error: `Unknown provider: ${provider}` }, { status: 404 });
|
||||
}
|
||||
|
||||
const workspaceId = req.nextUrl.searchParams.get('workspace');
|
||||
if (!workspaceId) {
|
||||
return NextResponse.json({ error: 'workspace query param is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const rawBody = Buffer.from(await req.arrayBuffer());
|
||||
|
||||
if (!entry.verify(rawBody, req.headers)) {
|
||||
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(rawBody.toString('utf-8'));
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Slack sends a challenge request during webhook setup — respond immediately.
|
||||
if (
|
||||
provider === 'slack' &&
|
||||
typeof parsed === 'object' &&
|
||||
parsed !== null &&
|
||||
'type' in parsed &&
|
||||
(parsed as { type: string }).type === 'url_verification'
|
||||
) {
|
||||
return NextResponse.json({ challenge: (parsed as { challenge?: string }).challenge ?? '' });
|
||||
}
|
||||
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = entry.ingester.parsePayload(parsed);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Payload validation failed';
|
||||
return NextResponse.json({ error: message }, { status: 422 });
|
||||
}
|
||||
|
||||
const result = entry.ingester.ingest(payload);
|
||||
const db = serverClient();
|
||||
|
||||
try {
|
||||
const origin = await createOrigin(db, result.origin);
|
||||
|
||||
const meta: Record<string, unknown> = {
|
||||
x: 120,
|
||||
y: 100,
|
||||
width: 360,
|
||||
height: 240,
|
||||
};
|
||||
if (result.renderUrl !== undefined) meta['renderUrl'] = result.renderUrl;
|
||||
|
||||
const artboard = await createArtboard(db, {
|
||||
workspace_id: workspaceId,
|
||||
name: result.artboardTitle,
|
||||
origin_id: origin.id,
|
||||
parent_artboard_id: null,
|
||||
metadata_jsonb: meta,
|
||||
});
|
||||
|
||||
return NextResponse.json({ originId: origin.id, artboardId: artboard.id }, { status: 201 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Database error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// GET /api/workspace
|
||||
// Returns the authenticated user's workspace. Auto-creates one on first visit
|
||||
// (FREE plan, name derived from Clerk user display name or email).
|
||||
|
||||
import { auth, currentUser } from '@clerk/nextjs/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import type { Workspace, InsertWorkspace } from '@originmain/origin-graph';
|
||||
|
||||
export async function GET() {
|
||||
const { userId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const db = serverClient();
|
||||
|
||||
// Look up an existing workspace owned by this Clerk user.
|
||||
const { data: existing, error: findError } = await db
|
||||
.from('workspaces')
|
||||
.select('*')
|
||||
.eq('owner_id', userId)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (findError && findError.code !== 'PGRST116') {
|
||||
return NextResponse.json({ error: findError.message }, { status: 500 });
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
return NextResponse.json(existing as Workspace);
|
||||
}
|
||||
|
||||
// No workspace yet — auto-create one. Fetch the Clerk user for a friendly name.
|
||||
const user = await currentUser();
|
||||
const name =
|
||||
user?.fullName ??
|
||||
user?.emailAddresses[0]?.emailAddress ??
|
||||
'My Workspace';
|
||||
|
||||
const insert: InsertWorkspace = {
|
||||
owner_id: userId,
|
||||
name,
|
||||
plan: 'FREE',
|
||||
settings_jsonb: {},
|
||||
};
|
||||
|
||||
const { data: created, error: insertError } = await db
|
||||
.from('workspaces')
|
||||
.insert(insert)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (insertError) {
|
||||
return NextResponse.json({ error: insertError.message }, { status: 500 });
|
||||
}
|
||||
|
||||
// Also add the owner as a team member.
|
||||
await db.from('team_members').insert({
|
||||
workspace_id: (created as Workspace).id,
|
||||
user_id: userId,
|
||||
role: 'OWNER',
|
||||
});
|
||||
|
||||
return NextResponse.json(created as Workspace, { status: 201 });
|
||||
}
|
||||
@@ -1,13 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { FluentProvider } from '@fluentui/react-components';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { originmainLightTheme } from '@originmain/ui';
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
// TanStack Query v5: create QueryClient inside useState so it's stable across
|
||||
// re-renders and each client-side navigation gets the same instance. Creating
|
||||
// it outside the component would cause it to be shared across requests on the
|
||||
// server (SSR memory leak / cross-request data pollution).
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30_000, // treat data as fresh for 30 s
|
||||
retry: 1, // one automatic retry on transient errors
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<FluentProvider theme={originmainLightTheme} style={{ height: '100%' }}>
|
||||
{children}
|
||||
</FluentProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useCanvas } from '@/store/canvas';
|
||||
import { LiveArtboard } from './LiveArtboard';
|
||||
import { SelectionOverlay } from './SelectionOverlay';
|
||||
import type { FiberNode } from '@originmain/renderer';
|
||||
|
||||
interface ArtboardProps {
|
||||
id: string;
|
||||
@@ -9,11 +13,19 @@ interface ArtboardProps {
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
renderUrl?: string;
|
||||
}
|
||||
|
||||
export function Artboard({ id, label, x, y, width, height }: ArtboardProps) {
|
||||
export function Artboard({ id, label, x, y, width, height, renderUrl }: ArtboardProps) {
|
||||
const { selectedArtboardId, selectArtboard } = useCanvas();
|
||||
const selected = selectedArtboardId === id;
|
||||
const [fiberRoot, setFiberRoot] = useState<FiberNode | undefined>(undefined);
|
||||
|
||||
const handleFiberUpdate = useCallback((root: FiberNode) => setFiberRoot(root), []);
|
||||
const handleComponentSelected = useCallback(
|
||||
(nodeId: string) => { void nodeId; /* future: highlight in inspector */ },
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -66,7 +78,26 @@ export function Artboard({ id, label, x, y, width, height }: ArtboardProps) {
|
||||
)}
|
||||
|
||||
{/* Per-artboard content */}
|
||||
{renderUrl ? (
|
||||
<>
|
||||
<LiveArtboard
|
||||
id={id}
|
||||
src={renderUrl}
|
||||
width={width}
|
||||
height={height}
|
||||
onFiberTreeUpdate={handleFiberUpdate}
|
||||
onComponentSelected={handleComponentSelected}
|
||||
/>
|
||||
<SelectionOverlay
|
||||
artboardId={id}
|
||||
{...(fiberRoot !== undefined ? { fiberRoot } : {})}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ArtboardContent id={id} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,15 +3,10 @@
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
import { useViewport } from '@/store/viewport';
|
||||
import { useCanvas } from '@/store/canvas';
|
||||
import { useWorkspace } from '@/hooks/useWorkspace';
|
||||
import { useArtboards } from '@/hooks/useArtboards';
|
||||
import { Artboard } from './Artboard';
|
||||
|
||||
const ARTBOARDS = [
|
||||
{ id: 'dashboard-card', label: 'DashboardCard', x: 120, y: 100, width: 280, height: 200 },
|
||||
{ id: 'user-profile', label: 'UserProfile', x: 460, y: 100, width: 200, height: 260 },
|
||||
{ id: 'nav-sidebar', label: 'NavSidebar', x: 120, y: 360, width: 200, height: 340 },
|
||||
{ id: 'data-table', label: 'DataTable', x: 380, y: 380, width: 420, height: 280 },
|
||||
];
|
||||
|
||||
export function Canvas() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const panX = useViewport((s) => s.panX);
|
||||
@@ -19,6 +14,9 @@ export function Canvas() {
|
||||
const zoom = useViewport((s) => s.zoom);
|
||||
const { activeTool, selectArtboard } = useCanvas();
|
||||
|
||||
const { data: workspace } = useWorkspace();
|
||||
const { artboards } = useArtboards(workspace?.id);
|
||||
|
||||
const isPanning = useRef(false);
|
||||
const lastPos = useRef({ x: 0, y: 0 });
|
||||
const spaceDown = useRef(false);
|
||||
@@ -124,7 +122,7 @@ export function Canvas() {
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{ARTBOARDS.map((ab) => (
|
||||
{artboards.map((ab) => (
|
||||
<Artboard key={ab.id} {...ab} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { Toolbar } from './Toolbar';
|
||||
import { ArtboardNavigator } from '../navigator/ArtboardNavigator';
|
||||
import { Canvas } from '../canvas/Canvas';
|
||||
import { Inspector } from '../inspector/Inspector';
|
||||
import { useHistory } from '@/store/history';
|
||||
import { useCanvas } from '@/store/canvas';
|
||||
|
||||
export function AppChrome() {
|
||||
const selectedArtboardId = useCanvas((s) => s.selectedArtboardId);
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (!(e.metaKey || e.ctrlKey)) return;
|
||||
if (!selectedArtboardId) return;
|
||||
|
||||
if (e.key === 'z' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
useHistory.getState().undo(selectedArtboardId);
|
||||
} else if ((e.key === 'z' && e.shiftKey) || e.key === 'y') {
|
||||
e.preventDefault();
|
||||
useHistory.getState().redo(selectedArtboardId);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [selectedArtboardId]);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useFileTree, FileTree, useFileTreeSelection } from '@pierre/trees/react';
|
||||
import { useFileTree, FileTree } from '@pierre/trees/react';
|
||||
import { themeToTreeStyles } from '@pierre/trees';
|
||||
import { SquareRegular } from '@fluentui/react-icons';
|
||||
import { useCanvas } from '@/store/canvas';
|
||||
|
||||
const ARTBOARDS = [
|
||||
{ id: 'dashboard-card', label: 'DashboardCard' },
|
||||
{ id: 'user-profile', label: 'UserProfile' },
|
||||
{ id: 'nav-sidebar', label: 'NavSidebar' },
|
||||
{ id: 'data-table', label: 'DataTable' },
|
||||
];
|
||||
import { useWorkspace } from '@/hooks/useWorkspace';
|
||||
import { useArtboards } from '@/hooks/useArtboards';
|
||||
|
||||
const FILE_PATHS = [
|
||||
'src/components/DashboardCard.tsx',
|
||||
@@ -54,6 +49,8 @@ const treeThemeStyles = themeToTreeStyles({
|
||||
|
||||
export function ArtboardNavigator() {
|
||||
const { selectedArtboardId, selectArtboard } = useCanvas();
|
||||
const { data: workspace } = useWorkspace();
|
||||
const { artboards } = useArtboards(workspace?.id);
|
||||
|
||||
const { model } = useFileTree({
|
||||
paths: FILE_PATHS,
|
||||
@@ -78,7 +75,7 @@ export function ArtboardNavigator() {
|
||||
{/* ── Artboards ── */}
|
||||
<SectionLabel>Artboards</SectionLabel>
|
||||
<div style={{ padding: '2px 6px 0' }}>
|
||||
{ARTBOARDS.map((ab) => {
|
||||
{artboards.map((ab) => {
|
||||
const sel = selectedArtboardId === ab.id;
|
||||
return (
|
||||
<NavRow
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { Artboard } from '@originmain/origin-graph';
|
||||
|
||||
export interface CanvasArtboard {
|
||||
id: string;
|
||||
label: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
renderUrl?: string;
|
||||
}
|
||||
|
||||
const DEMO_ARTBOARDS: CanvasArtboard[] = [
|
||||
{ id: 'dashboard-card', label: 'DashboardCard', x: 120, y: 100, width: 280, height: 200 },
|
||||
{ id: 'user-profile', label: 'UserProfile', x: 460, y: 100, width: 200, height: 260 },
|
||||
{ id: 'nav-sidebar', label: 'NavSidebar', x: 120, y: 360, width: 200, height: 340 },
|
||||
{ id: 'data-table', label: 'DataTable', x: 380, y: 380, width: 420, height: 280 },
|
||||
];
|
||||
|
||||
function toCanvasArtboard(ab: Artboard): CanvasArtboard | null {
|
||||
const meta = ab.metadata_jsonb;
|
||||
const x = typeof meta['x'] === 'number' ? meta['x'] : null;
|
||||
const y = typeof meta['y'] === 'number' ? meta['y'] : null;
|
||||
const width = typeof meta['width'] === 'number' ? meta['width'] : null;
|
||||
const height = typeof meta['height'] === 'number' ? meta['height'] : null;
|
||||
if (x === null || y === null || width === null || height === null) return null;
|
||||
const base: CanvasArtboard = { id: ab.id, label: ab.name, x, y, width, height };
|
||||
if (typeof meta['renderUrl'] === 'string') base.renderUrl = meta['renderUrl'];
|
||||
return base;
|
||||
}
|
||||
|
||||
async function fetchArtboards(workspaceId: string): Promise<CanvasArtboard[]> {
|
||||
const res = await fetch(`/api/artboards?workspaceId=${encodeURIComponent(workspaceId)}`);
|
||||
if (!res.ok) throw new Error(`Artboard fetch failed: ${res.status}`);
|
||||
const rows = (await res.json()) as Artboard[];
|
||||
return rows.map(toCanvasArtboard).filter((ab): ab is CanvasArtboard => ab !== null);
|
||||
}
|
||||
|
||||
export function useArtboards(workspaceId: string | undefined) {
|
||||
const query = useQuery({
|
||||
queryKey: ['artboards', workspaceId],
|
||||
queryFn: () => fetchArtboards(workspaceId!),
|
||||
enabled: workspaceId !== undefined,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
|
||||
// Show demo artboards while loading or when workspace has no artboards yet.
|
||||
const artboards =
|
||||
!query.data || query.data.length === 0 ? DEMO_ARTBOARDS : query.data;
|
||||
|
||||
return { artboards, isLoading: query.isLoading, error: query.error };
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { Workspace } from '@originmain/origin-graph';
|
||||
|
||||
async function fetchWorkspace(): Promise<Workspace> {
|
||||
const res = await fetch('/api/workspace');
|
||||
if (!res.ok) throw new Error(`Workspace fetch failed: ${res.status}`);
|
||||
return res.json() as Promise<Workspace>;
|
||||
}
|
||||
|
||||
export function useWorkspace() {
|
||||
return useQuery({
|
||||
queryKey: ['workspace'],
|
||||
queryFn: fetchWorkspace,
|
||||
staleTime: 5 * 60_000, // workspace data rarely changes
|
||||
retry: 2,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// ── Supabase client helpers ───────────────────────────────────────────────────
|
||||
// browserClient() — uses the anon key; safe to call on the client side.
|
||||
// RLS policies apply (currently: deny all anon).
|
||||
// serverClient() — uses the service-role key; ONLY call from Server
|
||||
// Components or API route handlers. Never expose to the
|
||||
// browser. Service role bypasses RLS entirely.
|
||||
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import type { DbClient } from '@originmain/origin-graph';
|
||||
|
||||
function requireEnv(name: string): string {
|
||||
const val = process.env[name];
|
||||
if (!val) throw new Error(`Missing required environment variable: ${name}`);
|
||||
return val;
|
||||
}
|
||||
|
||||
/** Browser-safe Supabase client (anon key, RLS enforced). */
|
||||
export function browserClient(): DbClient {
|
||||
return createClient(
|
||||
requireEnv('NEXT_PUBLIC_SUPABASE_URL'),
|
||||
requireEnv('NEXT_PUBLIC_SUPABASE_ANON_KEY'),
|
||||
) as unknown as DbClient;
|
||||
}
|
||||
|
||||
/** Server-only Supabase client (service-role key, bypasses RLS). */
|
||||
export function serverClient(): DbClient {
|
||||
return createClient(
|
||||
requireEnv('NEXT_PUBLIC_SUPABASE_URL'),
|
||||
requireEnv('SUPABASE_SERVICE_ROLE_KEY'),
|
||||
// Disable the auto-refresh token flow — this client never runs in a browser.
|
||||
{ auth: { autoRefreshToken: false, persistSession: false } },
|
||||
) as unknown as DbClient;
|
||||
}
|
||||
@@ -62,12 +62,13 @@ export const useHistory = create<HistoryStore>((set, get) => ({
|
||||
},
|
||||
|
||||
undo(artboardId) {
|
||||
const stack = get().stacks[artboardId] ?? emptyStack();
|
||||
const last = stack.past[stack.past.length - 1];
|
||||
if (!last) return undefined;
|
||||
|
||||
// Read + write atomically inside set() to avoid TOCTOU between get() and set().
|
||||
let undone: EditEntry | undefined;
|
||||
set(state => {
|
||||
const current = state.stacks[artboardId] ?? emptyStack();
|
||||
const last = current.past[current.past.length - 1];
|
||||
if (!last) return state; // nothing to undo — no-op
|
||||
undone = last;
|
||||
return {
|
||||
stacks: {
|
||||
...state.stacks,
|
||||
@@ -78,17 +79,17 @@ export const useHistory = create<HistoryStore>((set, get) => ({
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return last;
|
||||
return undone;
|
||||
},
|
||||
|
||||
redo(artboardId) {
|
||||
const stack = get().stacks[artboardId] ?? emptyStack();
|
||||
const next = stack.future[0];
|
||||
if (!next) return undefined;
|
||||
|
||||
// Read + write atomically inside set() to avoid TOCTOU between get() and set().
|
||||
let redone: EditEntry | undefined;
|
||||
set(state => {
|
||||
const current = state.stacks[artboardId] ?? emptyStack();
|
||||
const next = current.future[0];
|
||||
if (!next) return state; // nothing to redo — no-op
|
||||
redone = next;
|
||||
return {
|
||||
stacks: {
|
||||
...state.stacks,
|
||||
@@ -99,8 +100,7 @@ export const useHistory = create<HistoryStore>((set, get) => ({
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return next;
|
||||
return redone;
|
||||
},
|
||||
|
||||
canUndo(artboardId) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7,11 +7,13 @@
|
||||
import type {
|
||||
Artboard,
|
||||
Workspace,
|
||||
Origin,
|
||||
IntentDiff,
|
||||
DesignLanguageFile,
|
||||
AgentSession,
|
||||
TeamMember,
|
||||
InsertArtboard,
|
||||
InsertOrigin,
|
||||
InsertIntentDiff,
|
||||
InsertAgentSession,
|
||||
DiffStatus,
|
||||
@@ -166,6 +168,18 @@ export async function getActiveDesignLanguageFile(
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Origin queries ────────────────────────────────────────────────────────────
|
||||
|
||||
export async function createOrigin(db: DbClient, row: InsertOrigin): Promise<Origin> {
|
||||
const { data, error } = await (db
|
||||
.from('origins')
|
||||
.insert(row)
|
||||
.select()
|
||||
.single() as Promise<{ data: Origin; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Agent session queries ─────────────────────────────────────────────────────
|
||||
|
||||
export async function createAgentSession(db: DbClient, row: InsertAgentSession): Promise<AgentSession> {
|
||||
|
||||
Generated
+118
@@ -70,6 +70,9 @@ importers:
|
||||
|
||||
packages/app:
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk':
|
||||
specifier: ^0.56.0
|
||||
version: 0.56.0
|
||||
'@clerk/nextjs':
|
||||
specifier: ^7.2.7
|
||||
version: 7.2.7(next@15.5.15(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
@@ -79,12 +82,30 @@ importers:
|
||||
'@fluentui/react-icons':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.324(react@19.2.5)
|
||||
'@originmain/agent-bridge':
|
||||
specifier: workspace:*
|
||||
version: link:../agent-bridge
|
||||
'@originmain/ai-layer':
|
||||
specifier: workspace:*
|
||||
version: link:../ai-layer
|
||||
'@originmain/design-language':
|
||||
specifier: workspace:*
|
||||
version: link:../design-language
|
||||
'@originmain/diff-engine':
|
||||
specifier: workspace:*
|
||||
version: link:../diff-engine
|
||||
'@originmain/integrations':
|
||||
specifier: workspace:*
|
||||
version: link:../integrations
|
||||
'@originmain/multiplayer':
|
||||
specifier: workspace:*
|
||||
version: link:../multiplayer
|
||||
'@originmain/origin-graph':
|
||||
specifier: workspace:*
|
||||
version: link:../origin-graph
|
||||
'@originmain/platform':
|
||||
specifier: workspace:*
|
||||
version: link:../platform
|
||||
'@originmain/renderer':
|
||||
specifier: workspace:*
|
||||
version: link:../renderer
|
||||
@@ -97,6 +118,9 @@ importers:
|
||||
'@pierre/trees':
|
||||
specifier: 1.0.0-beta.3
|
||||
version: 1.0.0-beta.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@supabase/supabase-js':
|
||||
specifier: ^2.0.0
|
||||
version: 2.104.1
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.62.0
|
||||
version: 5.99.2(react@19.2.5)
|
||||
@@ -1510,6 +1534,33 @@ packages:
|
||||
'@stablelib/base64@1.0.1':
|
||||
resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
|
||||
|
||||
'@supabase/auth-js@2.104.1':
|
||||
resolution: {integrity: sha512-pqFnDKekq1isqlqnzqzyJ3mzmho+o+FjfVTqhKY3PFlwj2anx3OPznO1kbo1ZEwD8zg1r4EAFf/7pplLyX0ocQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@supabase/functions-js@2.104.1':
|
||||
resolution: {integrity: sha512-JjAH4JN9rZzxh4plQnILPrQZXAG6ccoRS6z9hQAGmXpRSwJA+7CWbsDV2R82I8MROlGDsjqj1Ot/cWpTfdf6xg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@supabase/phoenix@0.4.0':
|
||||
resolution: {integrity: sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==}
|
||||
|
||||
'@supabase/postgrest-js@2.104.1':
|
||||
resolution: {integrity: sha512-RqlLpvgXsjcc27fLyHNGm3zN0KDWXbkdTdaFtaEdX83RsTEqH7BAmshH7zoUMml5lL04naUeRjS3B81O6jZcJw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@supabase/realtime-js@2.104.1':
|
||||
resolution: {integrity: sha512-dVJHhFB2ErBd0/2qE9G8CedCrGoAtBfL9Q4zbSMXO7b1Cpld916ljSiX21mURUqijPf1WoPQG4Bp/averUzk/g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@supabase/storage-js@2.104.1':
|
||||
resolution: {integrity: sha512-2bQaLbkRshctkUVuqamwYZDEd+0cGSc9DY9sjh92DcA5hu1F/1AP8p6gxGr76sgdK9Ngi0rh+2Kdh+uC4hcnGA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@supabase/supabase-js@2.104.1':
|
||||
resolution: {integrity: sha512-E0H/CtVmaGjiAy+ieZ5ZB/1EqxXcGdaFaAc23AE5zaYfz6NtCNDcmaEdoGPYMPFH5pE6drGG6e3ljPmkFoGVxQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
@@ -1553,6 +1604,9 @@ packages:
|
||||
'@types/unist@3.0.3':
|
||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.59.0':
|
||||
resolution: {integrity: sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -1961,6 +2015,10 @@ packages:
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
iceberg-js@0.8.1:
|
||||
resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -2504,6 +2562,18 @@ packages:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ws@8.20.0:
|
||||
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -4227,6 +4297,46 @@ snapshots:
|
||||
|
||||
'@stablelib/base64@1.0.1': {}
|
||||
|
||||
'@supabase/auth-js@2.104.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@supabase/functions-js@2.104.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@supabase/phoenix@0.4.0': {}
|
||||
|
||||
'@supabase/postgrest-js@2.104.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@supabase/realtime-js@2.104.1':
|
||||
dependencies:
|
||||
'@supabase/phoenix': 0.4.0
|
||||
'@types/ws': 8.18.1
|
||||
tslib: 2.8.1
|
||||
ws: 8.20.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@supabase/storage-js@2.104.1':
|
||||
dependencies:
|
||||
iceberg-js: 0.8.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@supabase/supabase-js@2.104.1':
|
||||
dependencies:
|
||||
'@supabase/auth-js': 2.104.1
|
||||
'@supabase/functions-js': 2.104.1
|
||||
'@supabase/postgrest-js': 2.104.1
|
||||
'@supabase/realtime-js': 2.104.1
|
||||
'@supabase/storage-js': 2.104.1
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -4270,6 +4380,10 @@ snapshots:
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 22.19.17
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
@@ -4731,6 +4845,8 @@ snapshots:
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
iceberg-js@0.8.1: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
ignore@7.0.5: {}
|
||||
@@ -5315,6 +5431,8 @@ snapshots:
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
ws@8.20.0: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- Originmain — Initial Schema
|
||||
-- Run: supabase db push (or paste into Supabase SQL Editor)
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ── Extensions ───────────────────────────────────────────────────────────────
|
||||
create extension if not exists "uuid-ossp";
|
||||
|
||||
-- ── Enums (CHECK constraints on text columns) ─────────────────────────────
|
||||
-- Using text + CHECK rather than postgres enums so adding values is a
|
||||
-- non-destructive ALTER rather than a DROP/RE-CREATE cycle.
|
||||
|
||||
-- ── Tables ───────────────────────────────────────────────────────────────────
|
||||
|
||||
-- workspaces
|
||||
create table if not exists workspaces (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
name text not null,
|
||||
owner_id text not null, -- Clerk user ID
|
||||
plan text not null default 'FREE'
|
||||
check (plan in ('FREE','TEAM','ENTERPRISE')),
|
||||
settings_jsonb jsonb not null default '{}',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists workspaces_owner_id_idx on workspaces (owner_id);
|
||||
|
||||
-- origins
|
||||
create table if not exists origins (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
type text not null
|
||||
check (type in ('GIT_COMMIT','LINEAR_ISSUE','SLACK_MESSAGE','URL','FORK')),
|
||||
source_ref text not null,
|
||||
source_metadata_jsonb jsonb not null default '{}',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- artboards
|
||||
create table if not exists artboards (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
workspace_id uuid not null references workspaces (id) on delete cascade,
|
||||
name text not null,
|
||||
origin_id uuid references origins (id) on delete set null,
|
||||
parent_artboard_id uuid references artboards (id) on delete set null,
|
||||
-- metadata_jsonb stores canvas position/size and optional renderUrl:
|
||||
-- { x, y, width, height, renderUrl? }
|
||||
metadata_jsonb jsonb not null default '{}',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists artboards_workspace_id_idx on artboards (workspace_id);
|
||||
create index if not exists artboards_parent_artboard_id_idx on artboards (parent_artboard_id);
|
||||
create index if not exists artboards_created_at_idx on artboards (created_at desc);
|
||||
|
||||
-- intent_diffs
|
||||
create table if not exists intent_diffs (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
artboard_id uuid not null references artboards (id) on delete cascade,
|
||||
author_id text not null, -- Clerk user ID
|
||||
changes_jsonb jsonb not null default '{}',
|
||||
summary text not null default '',
|
||||
status text not null default 'DRAFT'
|
||||
check (status in ('DRAFT','EXPORTED','IMPLEMENTED','BLOCKED')),
|
||||
notes text,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists intent_diffs_artboard_id_idx on intent_diffs (artboard_id);
|
||||
create index if not exists intent_diffs_status_idx on intent_diffs (status);
|
||||
create index if not exists intent_diffs_created_at_idx on intent_diffs (created_at desc);
|
||||
|
||||
-- agent_sessions
|
||||
create table if not exists agent_sessions (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
artboard_id uuid not null references artboards (id) on delete cascade,
|
||||
diff_id uuid references intent_diffs (id) on delete set null,
|
||||
agent_type text not null
|
||||
check (agent_type in ('CURSOR','CLAUDE_CODE','GENERIC')),
|
||||
messages_jsonb jsonb not null default '[]',
|
||||
status text not null default 'ACTIVE'
|
||||
check (status in ('ACTIVE','COMPLETED','FAILED')),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists agent_sessions_artboard_id_idx on agent_sessions (artboard_id);
|
||||
create index if not exists agent_sessions_diff_id_idx on agent_sessions (diff_id);
|
||||
|
||||
-- design_language_files
|
||||
create table if not exists design_language_files (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
workspace_id uuid not null references workspaces (id) on delete cascade,
|
||||
name text not null,
|
||||
schema_jsonb jsonb not null default '{}',
|
||||
version integer not null default 1,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists design_language_files_workspace_id_idx on design_language_files (workspace_id);
|
||||
create index if not exists design_language_files_version_idx on design_language_files (workspace_id, version desc);
|
||||
|
||||
-- team_members
|
||||
create table if not exists team_members (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
workspace_id uuid not null references workspaces (id) on delete cascade,
|
||||
user_id text not null, -- Clerk user ID
|
||||
role text not null default 'VIEWER'
|
||||
check (role in ('OWNER','DESIGNER','ENGINEER','PM','VIEWER')),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (workspace_id, user_id)
|
||||
);
|
||||
|
||||
create index if not exists team_members_workspace_id_idx on team_members (workspace_id);
|
||||
create index if not exists team_members_user_id_idx on team_members (user_id);
|
||||
|
||||
-- ── Artboard ancestry (closure table) ────────────────────────────────────────
|
||||
-- Maintained by triggers below. Stores every ancestor at any depth so hierarchy
|
||||
-- queries are O(1) lookups rather than recursive CTEs.
|
||||
create table if not exists artboard_ancestry (
|
||||
artboard_id uuid not null references artboards (id) on delete cascade,
|
||||
ancestor_id uuid not null references artboards (id) on delete cascade,
|
||||
depth integer not null,
|
||||
primary key (artboard_id, ancestor_id)
|
||||
);
|
||||
|
||||
create index if not exists artboard_ancestry_ancestor_idx on artboard_ancestry (ancestor_id);
|
||||
|
||||
-- ── Triggers: updated_at ─────────────────────────────────────────────────────
|
||||
|
||||
create or replace function set_updated_at()
|
||||
returns trigger language plpgsql as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace trigger workspaces_updated_at
|
||||
before update on workspaces
|
||||
for each row execute function set_updated_at();
|
||||
|
||||
create or replace trigger artboards_updated_at
|
||||
before update on artboards
|
||||
for each row execute function set_updated_at();
|
||||
|
||||
create or replace trigger intent_diffs_updated_at
|
||||
before update on intent_diffs
|
||||
for each row execute function set_updated_at();
|
||||
|
||||
create or replace trigger agent_sessions_updated_at
|
||||
before update on agent_sessions
|
||||
for each row execute function set_updated_at();
|
||||
|
||||
create or replace trigger design_language_files_updated_at
|
||||
before update on design_language_files
|
||||
for each row execute function set_updated_at();
|
||||
|
||||
create or replace trigger team_members_updated_at
|
||||
before update on team_members
|
||||
for each row execute function set_updated_at();
|
||||
|
||||
-- ── Trigger: artboard ancestry maintenance ────────────────────────────────────
|
||||
|
||||
create or replace function maintain_artboard_ancestry()
|
||||
returns trigger language plpgsql as $$
|
||||
begin
|
||||
-- Self-row (depth 0)
|
||||
insert into artboard_ancestry (artboard_id, ancestor_id, depth)
|
||||
values (new.id, new.id, 0)
|
||||
on conflict do nothing;
|
||||
|
||||
-- Inherit all ancestors of the parent
|
||||
if new.parent_artboard_id is not null then
|
||||
insert into artboard_ancestry (artboard_id, ancestor_id, depth)
|
||||
select new.id, ancestor_id, depth + 1
|
||||
from artboard_ancestry
|
||||
where artboard_id = new.parent_artboard_id
|
||||
on conflict do nothing;
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace trigger artboards_ancestry_insert
|
||||
after insert on artboards
|
||||
for each row execute function maintain_artboard_ancestry();
|
||||
|
||||
-- ── RPC: get_diffs_by_status ──────────────────────────────────────────────────
|
||||
-- Used by getDiffsByStatus() in origin-graph/queries.ts.
|
||||
-- Joins intent_diffs → artboards to scope by workspace_id.
|
||||
|
||||
create or replace function get_diffs_by_status(p_workspace_id uuid, p_status text)
|
||||
returns setof intent_diffs
|
||||
language sql stable as $$
|
||||
select d.*
|
||||
from intent_diffs d
|
||||
join artboards a on a.id = d.artboard_id
|
||||
where a.workspace_id = p_workspace_id
|
||||
and d.status = p_status
|
||||
order by d.created_at desc;
|
||||
$$;
|
||||
|
||||
-- ── Row Level Security ────────────────────────────────────────────────────────
|
||||
-- API routes use the service-role key (bypasses RLS). Policies below apply
|
||||
-- when the anon/browser key is used for direct-to-Supabase reads.
|
||||
-- All tables are locked down; only authenticated Postgres users may read.
|
||||
|
||||
alter table workspaces enable row level security;
|
||||
alter table origins enable row level security;
|
||||
alter table artboards enable row level security;
|
||||
alter table intent_diffs enable row level security;
|
||||
alter table agent_sessions enable row level security;
|
||||
alter table design_language_files enable row level security;
|
||||
alter table team_members enable row level security;
|
||||
alter table artboard_ancestry enable row level security;
|
||||
|
||||
-- Service role bypasses RLS automatically. For completeness, deny all anon.
|
||||
-- Add permissive policies when you add Supabase Auth integration.
|
||||
create policy "No anon access — workspaces" on workspaces for all to anon using (false);
|
||||
create policy "No anon access — origins" on origins for all to anon using (false);
|
||||
create policy "No anon access — artboards" on artboards for all to anon using (false);
|
||||
create policy "No anon access — intent_diffs" on intent_diffs for all to anon using (false);
|
||||
create policy "No anon access — agent_sessions" on agent_sessions for all to anon using (false);
|
||||
create policy "No anon access — design_language_files" on design_language_files for all to anon using (false);
|
||||
create policy "No anon access — team_members" on team_members for all to anon using (false);
|
||||
create policy "No anon access — artboard_ancestry" on artboard_ancestry for all to anon using (false);
|
||||
Reference in New Issue
Block a user