loki-mode 6.53.0 → 6.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/bin/postinstall.js +29 -0
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +11 -2
- package/web-app/Dockerfile +59 -0
- package/web-app/alembic.ini +43 -0
- package/web-app/auth.py +249 -0
- package/web-app/crypto.py +83 -0
- package/web-app/deploy/k8s/purple-lab/configmap.yaml +8 -0
- package/web-app/deploy/k8s/purple-lab/deployment.yaml +69 -0
- package/web-app/deploy/k8s/purple-lab/hpa.yaml +24 -0
- package/web-app/deploy/k8s/purple-lab/ingress.yaml +30 -0
- package/web-app/deploy/k8s/purple-lab/networkpolicy.yaml +82 -0
- package/web-app/deploy/k8s/purple-lab/pdb.yaml +11 -0
- package/web-app/deploy/k8s/purple-lab/postgres.yaml +84 -0
- package/web-app/deploy/k8s/purple-lab/pvc.yaml +10 -0
- package/web-app/deploy/k8s/purple-lab/secret.yaml +13 -0
- package/web-app/deploy/k8s/purple-lab/service.yaml +13 -0
- package/web-app/deploy/k8s/purple-lab/serviceaccount.yaml +7 -0
- package/web-app/dist/assets/{Badge-CnWBUi7C.js → Badge-BDr4DPCT.js} +1 -1
- package/web-app/dist/assets/{Button-5ThWFbkO.js → Button-WBFGRnUr.js} +1 -1
- package/web-app/dist/assets/{Card-CcTmaOCN.js → Card-DzOT34Rr.js} +1 -1
- package/web-app/dist/assets/{HomePage-Dx4Ae0hu.js → HomePage-B8kMCXMB.js} +1 -1
- package/web-app/dist/assets/{LoginPage-CRffqZNo.js → LoginPage-D9lCyiqM.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-B1QZ92yR.js → NotFoundPage-DzeZ0uQ6.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-BVnDGxXk.js → ProjectPage-C-k0iy0i.js} +14 -14
- package/web-app/dist/assets/{ProjectsPage-2Fi6cKB-.js → ProjectsPage-jys_pHzp.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-DOzGoyLv.js → SettingsPage-Cz_RXr82.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-B-f1Gfbg.js → TemplatesPage-COnhb_Wq.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-DrKIbiB8.js → TerminalOutput-CmdEXHHd.js} +1 -1
- package/web-app/dist/assets/{arrow-left-CFG0TEkb.js → arrow-left-DAZzI0L-.js} +1 -1
- package/web-app/dist/assets/{clock-C-GPrW5k.js → clock-BHGf6zSk.js} +1 -1
- package/web-app/dist/assets/{external-link-ujbkNBY4.js → external-link-DLYjfP9j.js} +1 -1
- package/web-app/dist/assets/{index-B8gGcUMo.js → index-B8Eg1YHL.js} +2 -2
- package/web-app/dist/index.html +1 -1
- package/web-app/docker-compose.purple-lab.yml +76 -0
- package/web-app/migrations/env.py +103 -0
- package/web-app/migrations/script.py.mako +25 -0
- package/web-app/migrations/versions/.gitkeep +0 -0
- package/web-app/migrations/versions/001_initial_schema.py +118 -0
- package/web-app/models.py +140 -0
- package/web-app/requirements.txt +27 -0
- package/web-app/server.py +158 -22
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as f,u as h,r as n,a as g,j as e}from"./index-
|
|
1
|
+
import{c as f,u as h,r as n,a as g,j as e}from"./index-B8Eg1YHL.js";import{B as d,P as m}from"./Button-WBFGRnUr.js";import{C as j}from"./Card-DzOT34Rr.js";import{u as y,B as b}from"./Badge-BDr4DPCT.js";import"./clock-BHGf6zSk.js";/**
|
|
2
2
|
* @license lucide-react v0.577.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as t,a,j as e}from"./index-
|
|
1
|
+
import{r as t,a,j as e}from"./index-B8Eg1YHL.js";import{C as l}from"./Card-DzOT34Rr.js";import{E as c}from"./external-link-DLYjfP9j.js";const h=[{id:"claude",name:"Claude",description:"Anthropic Claude Code -- full features"},{id:"codex",name:"Codex",description:"OpenAI Codex CLI -- degraded mode"},{id:"gemini",name:"Gemini",description:"Google Gemini CLI -- degraded mode"}];function p(){const[i,n]=t.useState("claude"),[o,r]=t.useState(!1),[d,x]=t.useState("");t.useEffect(()=>{a.getCurrentProvider().then(s=>n(s.provider)).catch(()=>{}),a.getStatus().then(s=>x(s.version||"")).catch(()=>{})},[]);const m=async s=>{n(s),r(!0);try{await a.setProvider(s)}catch{}finally{r(!1)}};return e.jsxs("div",{className:"max-w-[800px] mx-auto px-6 py-8",children:[e.jsx("h1",{className:"font-heading text-h1 text-[#36342E] mb-8",children:"Settings"}),e.jsxs("section",{className:"mb-10",children:[e.jsx("h2",{className:"text-sm font-semibold text-[#36342E] uppercase tracking-wide mb-4",children:"Provider"}),e.jsx("div",{className:"flex flex-col gap-3",children:h.map(s=>e.jsx(l,{hover:!0,onClick:()=>m(s.id),className:i===s.id?"ring-2 ring-[#553DE9] border-[#553DE9]":"",children:e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsx("div",{className:`w-4 h-4 rounded-full border-2 flex items-center justify-center flex-shrink-0 ${i===s.id?"border-[#553DE9]":"border-[#ECEAE3]"}`,children:i===s.id&&e.jsx("div",{className:"w-2 h-2 rounded-full bg-[#553DE9]"})}),e.jsxs("div",{children:[e.jsx("p",{className:"text-sm font-medium text-[#36342E]",children:s.name}),e.jsx("p",{className:"text-xs text-[#6B6960]",children:s.description})]})]})},s.id))}),o&&e.jsx("p",{className:"text-xs text-[#6B6960] mt-2",children:"Saving..."})]}),e.jsxs("section",{children:[e.jsx("h2",{className:"text-sm font-semibold text-[#36342E] uppercase tracking-wide mb-4",children:"About"}),e.jsx(l,{children:e.jsxs("div",{className:"flex flex-col gap-3",children:[d&&e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("span",{className:"text-sm text-[#6B6960]",children:"Version"}),e.jsxs("span",{className:"text-sm font-medium text-[#36342E]",children:["v",d]})]}),e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("span",{className:"text-sm text-[#6B6960]",children:"Documentation"}),e.jsxs("a",{href:"https://www.autonomi.dev/docs",target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center gap-1 text-sm text-[#553DE9] hover:underline",children:["autonomi.dev/docs ",e.jsx(c,{size:12})]})]}),e.jsxs("div",{className:"flex items-center justify-between",children:[e.jsx("span",{className:"text-sm text-[#6B6960]",children:"GitHub"}),e.jsxs("a",{href:"https://github.com/asklokesh/loki-mode",target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center gap-1 text-sm text-[#553DE9] hover:underline",children:["asklokesh/loki-mode ",e.jsx(c,{size:12})]})]})]})})]})]})}export{p as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{u as c,r,a as x,j as t}from"./index-
|
|
1
|
+
import{u as c,r,a as x,j as t}from"./index-B8Eg1YHL.js";import{C as p}from"./Card-DzOT34Rr.js";import{u as d,B as h}from"./Badge-BDr4DPCT.js";import"./clock-BHGf6zSk.js";const g=[{key:"all",label:"All"},{key:"website",label:"Website"},{key:"api",label:"API"},{key:"cli",label:"CLI"},{key:"bot",label:"Bot"},{key:"data",label:"Data"},{key:"other",label:"Other"}];function u(l){return l.replace(/\.md$/i,"").replace(/[-_]/g," ").replace(/\b\w/g,a=>a.toUpperCase())}function k(){const l=c(),[a,o]=r.useState("all"),n=r.useCallback(()=>x.getTemplates(),[]),{data:s}=d(n,6e4,!0),i=r.useMemo(()=>s?a==="all"?s:s.filter(e=>(e.category||"other")===a):[],[s,a]),m=e=>{sessionStorage.setItem("pl_template",e),l("/")};return t.jsxs("div",{className:"max-w-[1400px] mx-auto px-6 py-8",children:[t.jsx("h1",{className:"font-heading text-h1 text-[#36342E] mb-6",children:"Templates"}),t.jsx("div",{className:"flex items-center gap-1 mb-6",role:"tablist",children:g.map(e=>t.jsx("button",{role:"tab","aria-selected":a===e.key,onClick:()=>o(e.key),className:`px-3 py-1.5 text-xs font-semibold rounded-[3px] transition-colors ${a===e.key?"bg-[#553DE9] text-white":"text-[#6B6960] hover:text-[#36342E] hover:bg-[#F8F4F0]"}`,children:e.label},e.key))}),s?i.length===0?t.jsx("p",{className:"text-sm text-[#6B6960] py-12 text-center",children:"No templates in this category."}):t.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4",children:i.map(e=>t.jsxs(p,{hover:!0,onClick:()=>m(e.filename),children:[t.jsx("div",{className:"mb-2",children:t.jsx(h,{status:"version",children:e.category||"other"})}),t.jsx("h3",{className:"text-sm font-medium text-[#36342E] mb-1",children:u(e.name)}),t.jsx("p",{className:"text-xs text-[#6B6960]",children:e.filename})]},e.filename))}):t.jsx("p",{className:"text-sm text-[#6B6960]",children:"Loading templates..."})]})}export{k as default};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/HomePage-
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/HomePage-B8kMCXMB.js","assets/Badge-BDr4DPCT.js","assets/clock-BHGf6zSk.js","assets/TerminalOutput-CmdEXHHd.js","assets/ProjectPage-C-k0iy0i.js","assets/Button-WBFGRnUr.js","assets/arrow-left-DAZzI0L-.js","assets/external-link-DLYjfP9j.js","assets/ProjectPage-9CEnUXvW.css","assets/ProjectsPage-jys_pHzp.js","assets/Card-DzOT34Rr.js","assets/TemplatesPage-COnhb_Wq.js","assets/SettingsPage-Cz_RXr82.js","assets/NotFoundPage-DzeZ0uQ6.js"])))=>i.map(i=>d[i]);
|
|
2
2
|
var S0=Object.defineProperty;var b0=(i,o,r)=>o in i?S0(i,o,{enumerable:!0,configurable:!0,writable:!0,value:r}):i[o]=r;var Bn=(i,o,r)=>b0(i,typeof o!="symbol"?o+"":o,r);(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const d of document.querySelectorAll('link[rel="modulepreload"]'))f(d);new MutationObserver(d=>{for(const h of d)if(h.type==="childList")for(const g of h.addedNodes)g.tagName==="LINK"&&g.rel==="modulepreload"&&f(g)}).observe(document,{childList:!0,subtree:!0});function r(d){const h={};return d.integrity&&(h.integrity=d.integrity),d.referrerPolicy&&(h.referrerPolicy=d.referrerPolicy),d.crossOrigin==="use-credentials"?h.credentials="include":d.crossOrigin==="anonymous"?h.credentials="omit":h.credentials="same-origin",h}function f(d){if(d.ep)return;d.ep=!0;const h=r(d);fetch(d.href,h)}})();function E0(i){return i&&i.__esModule&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i}var Bf={exports:{}},Ln={};/**
|
|
3
3
|
* @license React
|
|
4
4
|
* react-jsx-runtime.production.js
|
|
@@ -183,4 +183,4 @@ Please change the parent <Route path="${B}"> to <Route path="${B==="/"?"*":`${B}
|
|
|
183
183
|
*
|
|
184
184
|
* This source code is licensed under the ISC license.
|
|
185
185
|
* See the LICENSE file in the root directory of this source tree.
|
|
186
|
-
*/const $1=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],Kh=Jt("x",$1),Jh=`${window.location.origin}/api`,W1=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws`;function F1(){const i={"Content-Type":"application/json"};try{const o=localStorage.getItem("pl_auth_token");o&&(i.Authorization=`Bearer ${o}`)}catch{}return i}async function W(i,o){const r=await fetch(`${Jh}${i}`,{...o,headers:{...F1(),...o==null?void 0:o.headers}});if(!r.ok){const f=await r.text().catch(()=>"");throw new Error(`API error ${r.status}: ${r.statusText}${f?` - ${f}`:""}`)}return r.json()}const qa={startSession:i=>W("/session/start",{method:"POST",body:JSON.stringify(i)}),stopSession:()=>W("/session/stop",{method:"POST"}),pauseSession:()=>W("/session/pause",{method:"POST"}),resumeSession:()=>W("/session/resume",{method:"POST"}),getPrdPrefill:()=>W("/session/prd-prefill"),getStatus:()=>W("/session/status"),getAgents:()=>W("/session/agents"),getLogs:(i=200)=>W(`/session/logs?lines=${i}`),getMemorySummary:()=>W("/session/memory"),getChecklist:()=>W("/session/checklist"),getFiles:()=>W("/session/files"),getFileContent:i=>W(`/session/files/content?path=${encodeURIComponent(i)}`),getSessionFileContent:(i,o)=>W(`/sessions/${encodeURIComponent(i)}/file?path=${encodeURIComponent(o)}`),getTemplates:()=>W("/templates"),getTemplateContent:i=>W(`/templates/${encodeURIComponent(i)}`),planSession:(i,o)=>W("/session/plan",{method:"POST",body:JSON.stringify({prd:i,provider:o})}),generateReport:(i="markdown")=>W("/session/report",{method:"POST",body:JSON.stringify({format:i})}),shareSession:()=>W("/session/share",{method:"POST"}),getCurrentProvider:()=>W("/provider/current"),setProvider:i=>W("/provider/set",{method:"POST",body:JSON.stringify({provider:i})}),getMetrics:()=>W("/session/metrics"),getSessionsHistory:()=>W("/sessions/history"),getSessionDetail:i=>W(`/sessions/${encodeURIComponent(i)}`),onboardRepo:i=>W("/session/onboard",{method:"POST",body:JSON.stringify({path:i})}),saveSessionFile:(i,o,r)=>W(`/sessions/${encodeURIComponent(i)}/file`,{method:"PUT",body:JSON.stringify({path:o,content:r})}),createSessionFile:(i,o,r="")=>W(`/sessions/${encodeURIComponent(i)}/file`,{method:"POST",body:JSON.stringify({path:o,content:r})}),deleteSessionFile:(i,o)=>W(`/sessions/${encodeURIComponent(i)}/file`,{method:"DELETE",body:JSON.stringify({path:o})}),createSessionDirectory:(i,o)=>W(`/sessions/${encodeURIComponent(i)}/directory`,{method:"POST",body:JSON.stringify({path:o})}),reviewProject:i=>W(`/sessions/${encodeURIComponent(i)}/review`,{method:"POST"}),testProject:i=>W(`/sessions/${encodeURIComponent(i)}/test`,{method:"POST"}),explainProject:i=>W(`/sessions/${encodeURIComponent(i)}/explain`,{method:"POST"}),exportProject:i=>W(`/sessions/${encodeURIComponent(i)}/export`,{method:"POST"}),chatMessage:(i,o,r="quick")=>W(`/sessions/${encodeURIComponent(i)}/chat`,{method:"POST",body:JSON.stringify({message:o,mode:r})}),chatStart:(i,o,r="quick")=>W(`/sessions/${encodeURIComponent(i)}/chat`,{method:"POST",body:JSON.stringify({message:o,mode:r})}),chatPoll:(i,o)=>W(`/sessions/${encodeURIComponent(i)}/chat/${encodeURIComponent(o)}`),chatStreamUrl:(i,o)=>`${Jh}/sessions/${encodeURIComponent(i)}/chat/${encodeURIComponent(o)}/stream`,chatCancel:(i,o)=>W(`/sessions/${encodeURIComponent(i)}/chat/${encodeURIComponent(o)}/cancel`,{method:"POST"}),getPreviewInfo:i=>W(`/sessions/${encodeURIComponent(i)}/preview-info`),devserver:{start:(i,o)=>W(`/sessions/${encodeURIComponent(i)}/devserver/start`,{method:"POST",body:JSON.stringify({command:o||null})}),stop:i=>W(`/sessions/${encodeURIComponent(i)}/devserver/stop`,{method:"POST"}),status:i=>W(`/sessions/${encodeURIComponent(i)}/devserver/status`)},getSecrets:()=>W("/secrets"),setSecret:(i,o)=>W("/secrets",{method:"POST",body:JSON.stringify({key:i,value:o})}),deleteSecret:i=>W(`/secrets/${encodeURIComponent(i)}`,{method:"DELETE"}),getMe:()=>W("/auth/me"),getGitHubAuthUrl:()=>W("/auth/github/url"),getGoogleAuthUrl:()=>W("/auth/google/url"),githubCallback:(i,o)=>W("/auth/github/callback",{method:"POST",body:JSON.stringify({code:i,state:o})}),googleCallback:(i,o,r)=>W("/auth/google/callback",{method:"POST",body:JSON.stringify({code:i,state:o,redirect_uri:r||`${window.location.origin}${window.location.pathname}`})})};class I1{constructor(o){Bn(this,"ws",null);Bn(this,"listeners",new Map);Bn(this,"reconnectTimer",null);Bn(this,"url");this.url=o||W1}connect(){var o;((o=this.ws)==null?void 0:o.readyState)!==WebSocket.OPEN&&(this.ws=new WebSocket(this.url),this.ws.onopen=()=>{this.emit("connected",{message:"WebSocket connected"})},this.ws.onmessage=r=>{try{const f=JSON.parse(r.data);this.emit(f.type,f.data||f)}catch{}},this.ws.onclose=()=>{this.emit("disconnected",{}),this.scheduleReconnect()},this.ws.onerror=()=>{var r;(r=this.ws)==null||r.close()})}scheduleReconnect(){this.reconnectTimer||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect()},3e3))}on(o,r){return this.listeners.has(o)||this.listeners.set(o,new Set),this.listeners.get(o).add(r),()=>{var f;return(f=this.listeners.get(o))==null?void 0:f.delete(r)}}emit(o,r){var f,d;(f=this.listeners.get(o))==null||f.forEach(h=>h(r)),(d=this.listeners.get("*"))==null||d.forEach(h=>h({type:o,data:r}))}send(o){var r;((r=this.ws)==null?void 0:r.readyState)===WebSocket.OPEN&&this.ws.send(JSON.stringify(o))}disconnect(){var o;this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(o=this.ws)==null||o.close(),this.ws=null}}const Vf="pl_auth_token",kh=E.createContext({user:null,loading:!0,login:()=>{},logout:()=>{},isLocalMode:!0});function P1(i,o,r){o(!1),r({email:i.sub||i.email||"",name:i.name||"",avatar_url:i.avatar||"",authenticated:!0})}function tg({children:i}){const[o,r]=E.useState(null),[f,d]=E.useState(!0),[h,g]=E.useState(!0),_=Ff(),S=je();E.useEffect(()=>{let j=!1;async function w(){const Z=new URLSearchParams(window.location.search),Y=Z.get("token"),G=Z.get("code");if(Y)localStorage.setItem(Vf,Y),window.history.replaceState({},"",window.location.pathname);else if(G)try{const B=sessionStorage.getItem("pl_oauth_provider")||"github",F=sessionStorage.getItem("pl_oauth_state")||"";sessionStorage.removeItem("pl_oauth_state"),sessionStorage.removeItem("pl_oauth_provider");let K;if(B==="google"?K=await qa.googleCallback(G,F):K=await qa.githubCallback(G,F),!j){localStorage.setItem(Vf,K.token),window.history.replaceState({},"",window.location.pathname),g(!1),r({email:K.user.email,name:K.user.name,avatar_url:K.user.avatar_url,authenticated:!0}),d(!1);return}}catch{window.history.replaceState({},"",window.location.pathname)}try{const B=await qa.getMe();if(j)return;B.local_mode?(g(!0),r(null)):B.authenticated?P1(B,g,r):(g(!1),r(null))}catch{if(j)return;g(!0),r(null)}finally{j||d(!1)}}return w(),()=>{j=!0}},[]);const y=E.useCallback(j=>{j==="github"?qa.getGitHubAuthUrl().then(w=>{try{const Y=new URL(w.url).searchParams.get("state");Y&&(sessionStorage.setItem("pl_oauth_state",Y),sessionStorage.setItem("pl_oauth_provider","github"))}catch{}window.location.href=w.url}).catch(()=>{}):j==="google"&&qa.getGoogleAuthUrl().then(w=>{try{const Y=new URL(w.url).searchParams.get("state");Y&&(sessionStorage.setItem("pl_oauth_state",Y),sessionStorage.setItem("pl_oauth_provider","google"))}catch{}window.location.href=w.url}).catch(()=>{})},[]),M=E.useCallback(()=>{localStorage.removeItem(Vf),r(null),S.pathname!=="/login"&&_("/login",{replace:!0})},[_,S.pathname]),z=E.useMemo(()=>({user:o,loading:f,login:y,logout:M,isLocalMode:h}),[o,f,y,M,h]);return O.jsx(kh.Provider,{value:z,children:i})}function $h(){return E.useContext(kh)}const Th="pl_sidebar_collapsed",eg=[{to:"/",label:"Home",icon:N1},{to:"/projects",label:"Projects",icon:M1},{to:"/templates",label:"Templates",icon:U1}],lg=[{to:"/settings",label:"Settings",icon:Vh}];function ag(){const[i,o]=E.useState(typeof window<"u"?window.innerWidth<768:!1);return E.useEffect(()=>{const r=()=>o(window.innerWidth<768);return window.addEventListener("resize",r),()=>window.removeEventListener("resize",r)},[]),i}function ng({wsConnected:i,version:o}){const r=ag(),f=je(),[d,h]=E.useState(!1),[g,_]=E.useState(()=>{try{return localStorage.getItem(Th)==="1"}catch{return!1}});E.useEffect(()=>{try{localStorage.setItem(Th,g?"1":"0")}catch{}},[g]),E.useEffect(()=>{r&&h(!1)},[f.pathname,r]);const S=r?d:!g,y=S?240:64,M=j=>["flex items-center gap-3 px-3 py-2 text-sm transition-colors rounded-[5px]",!S&&"justify-center",j?"bg-[#553DE9]/8 text-[#553DE9] font-medium border-l-2 border-[#553DE9]":"text-[#36342E] hover:bg-[#F8F4F0]"].filter(Boolean).join(" "),z=O.jsxs("aside",{className:"flex flex-col h-full border-r border-[#ECEAE3] bg-white transition-[width] duration-200",style:{width:y,minWidth:y},children:[O.jsxs("div",{className:"flex items-center justify-between px-4 h-14 border-b border-[#ECEAE3]",children:[S&&O.jsxs("div",{className:"flex flex-col",children:[O.jsx("span",{className:"font-heading text-lg font-bold leading-tight text-[#36342E]",children:"Purple Lab"}),O.jsx("span",{className:"text-xs text-[#6B6960]",children:"Powered by Loki"})]}),r?O.jsx("button",{type:"button","aria-label":d?"Close menu":"Open menu",title:d?"Close menu":"Open menu",onClick:()=>h(!d),className:"inline-flex items-center justify-center w-7 h-7 rounded-[3px] text-[#939084] hover:bg-[#F8F4F0] transition-colors",children:d?O.jsx(Kh,{size:16}):O.jsx(L1,{size:16})}):O.jsx("button",{type:"button","aria-label":g?"Expand sidebar":"Collapse sidebar",title:g?"Expand sidebar":"Collapse sidebar",onClick:()=>_(!g),className:"inline-flex items-center justify-center w-7 h-7 rounded-[3px] text-[#939084] hover:bg-[#F8F4F0] transition-colors",children:g?O.jsx(V1,{size:16}):O.jsx(Q1,{size:16})})]}),O.jsxs("nav",{className:"flex-1 px-2 py-3 flex flex-col gap-1","aria-label":"Main navigation",children:[eg.map(j=>O.jsxs(di,{to:j.to,end:j.to==="/",className:({isActive:w})=>M(w),title:S?void 0:j.label,children:[O.jsx(j.icon,{size:18}),S&&O.jsx("span",{children:j.label})]},j.to)),O.jsx("div",{className:"my-2 border-t border-[#ECEAE3]"}),lg.map(j=>O.jsxs(di,{to:j.to,className:({isActive:w})=>M(w),title:S?void 0:j.label,children:[O.jsx(j.icon,{size:18}),S&&O.jsx("span",{children:j.label})]},j.to))]}),O.jsxs("div",{className:"px-3 py-3 border-t border-[#ECEAE3] flex flex-col gap-2",children:[O.jsx(ug,{collapsed:!S}),O.jsxs("div",{className:["flex items-center gap-2 text-xs",!S&&"justify-center"].filter(Boolean).join(" "),children:[O.jsx("span",{className:`w-2 h-2 rounded-full flex-shrink-0 ${i?"bg-[#1FC5A8]":"bg-[#C45B5B]"}`}),S&&O.jsx("span",{className:"text-[#6B6960]",children:i?"Connected":"Disconnected"})]}),S&&o&&O.jsxs("span",{className:"text-xs text-[#6B6960]",children:["v",o]}),O.jsxs("a",{href:"https://www.autonomi.dev/docs",target:"_blank",rel:"noopener noreferrer",className:["flex items-center gap-2 text-xs text-[#6B6960] hover:text-[#36342E] transition-colors",!S&&"justify-center"].filter(Boolean).join(" "),title:S?void 0:"Documentation",children:[O.jsx(E1,{size:14}),S&&O.jsx("span",{children:"Docs"})]})]})]});return r&&d?O.jsxs(O.Fragment,{children:[O.jsx("div",{className:"fixed inset-0 z-40 bg-ink/20",onClick:()=>h(!1)}),O.jsx("div",{className:"fixed inset-y-0 left-0 z-50",children:z})]}):z}function ug({collapsed:i}){const{user:o,logout:r,isLocalMode:f}=$h(),[d,h]=E.useState(!1),g=E.useRef(null);return E.useEffect(()=>{function _(S){g.current&&!g.current.contains(S.target)&&h(!1)}if(d)return document.addEventListener("mousedown",_),()=>document.removeEventListener("mousedown",_)},[d]),f||!o?O.jsxs("div",{className:["flex items-center gap-2 text-xs",i&&"justify-center"].filter(Boolean).join(" "),title:i?"Local Mode":void 0,children:[O.jsx(G1,{size:14,className:"text-[#939084] flex-shrink-0"}),!i&&O.jsx("span",{className:"text-[#939084]",children:"Local Mode"})]}):O.jsxs("div",{className:"relative",ref:g,"data-testid":"user-section",children:[O.jsxs("button",{type:"button",onClick:()=>h(!d),className:["flex items-center gap-2 w-full text-left text-xs rounded-[3px] py-1 px-1 hover:bg-[#F8F4F0] transition-colors",i&&"justify-center"].filter(Boolean).join(" "),title:i?o.name||o.email:void 0,children:[o.avatar_url?O.jsx("img",{src:o.avatar_url,alt:"",className:"w-5 h-5 rounded-full flex-shrink-0"}):O.jsx("div",{className:"w-5 h-5 rounded-full bg-[#553DE9] flex items-center justify-center text-white text-[10px] font-bold flex-shrink-0",children:(o.name||o.email||"?")[0].toUpperCase()}),!i&&O.jsxs(O.Fragment,{children:[O.jsx("span",{className:"text-[#36342E] truncate flex-1",children:o.name||o.email}),O.jsx(T1,{size:12,className:`text-[#939084] transition-transform ${d?"":"rotate-180"}`})]})]}),d&&O.jsxs("div",{className:"absolute bottom-full left-0 mb-1 w-48 bg-white border border-[#ECEAE3] rounded-lg shadow-lg py-1 z-50",children:[O.jsxs("div",{className:"px-3 py-2 border-b border-[#ECEAE3]",children:[O.jsx("p",{className:"text-xs font-medium text-[#36342E] truncate",children:o.name}),O.jsx("p",{className:"text-xs text-[#939084] truncate",children:o.email})]}),O.jsxs(di,{to:"/settings",onClick:()=>h(!1),className:"flex items-center gap-2 px-3 py-2 text-xs text-[#36342E] hover:bg-[#F8F4F0] transition-colors",children:[O.jsx(Vh,{size:14}),"Settings"]}),O.jsxs("button",{type:"button",onClick:()=>{h(!1),r()},className:"flex items-center gap-2 px-3 py-2 text-xs text-[#C45B5B] hover:bg-[#F8F4F0] transition-colors w-full text-left",children:[O.jsx(H1,{size:14}),"Sign out"]})]})]})}const zh="pl_onboarding_complete",wn=[{icon:R1,title:"Write your PRD",description:"Describe what you want to build, or choose a template to get started quickly."},{icon:k1,title:"Use the terminal",description:"Run commands directly in the integrated terminal to install dependencies or debug."},{icon:_1,title:"Preview in real-time",description:"Switch to the Preview tab to see your app running with live reload."},{icon:Y1,title:"Iterate with AI Chat",description:"Ask the AI to modify, fix, or explain your code in the chat panel."}];function ig(){const[i,o]=E.useState(!1),[r,f]=E.useState(0);E.useEffect(()=>{try{localStorage.getItem(zh)!=="1"&&o(!0)}catch{}},[]);const d=()=>{o(!1);try{localStorage.setItem(zh,"1")}catch{}},h=()=>{r<wn.length-1?f(r+1):d()};if(!i)return null;const g=wn[r],_=g.icon;return O.jsx("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-ink/30",children:O.jsxs("div",{className:"bg-card rounded-card shadow-card-hover border border-border w-full max-w-sm mx-4",children:[O.jsxs("div",{className:"flex items-center justify-between px-5 pt-5 pb-2",children:[O.jsxs("span",{className:"text-[11px] font-mono text-muted-accessible",children:[r+1," / ",wn.length]}),O.jsx("button",{onClick:d,className:"text-muted hover:text-ink transition-colors p-1 rounded-btn hover:bg-hover",title:"Skip onboarding",children:O.jsx(Kh,{size:14})})]}),O.jsxs("div",{className:"px-5 pb-4 text-center",children:[O.jsx("div",{className:"w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-4",children:O.jsx(_,{size:24,className:"text-primary"})}),O.jsx("h3",{className:"text-sm font-heading font-bold text-ink mb-1",children:g.title}),O.jsx("p",{className:"text-xs text-muted-accessible leading-relaxed",children:g.description})]}),O.jsx("div",{className:"flex items-center justify-center gap-1.5 pb-4",children:wn.map((S,y)=>O.jsx("div",{className:`w-1.5 h-1.5 rounded-full transition-colors ${y===r?"bg-primary":"bg-border"}`},y))}),O.jsxs("div",{className:"flex items-center justify-between px-5 py-3 border-t border-border",children:[O.jsx("button",{onClick:d,className:"text-xs text-muted hover:text-ink transition-colors",children:"Skip"}),O.jsx("button",{onClick:h,className:"inline-flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium rounded-btn bg-primary text-white hover:bg-primary-hover transition-colors",children:r<wn.length-1?O.jsxs(O.Fragment,{children:["Next",O.jsx(S1,{size:12})]}):"Get Started"})]})]})})}function cg(i){const[o,r]=E.useState(!1),f=E.useRef(null),d=E.useRef(i);E.useEffect(()=>{d.current=i},[i]),E.useEffect(()=>{const g=new I1;return f.current=g,g.on("connected",()=>r(!0)),g.on("disconnected",()=>{r(!1),d.current&&d.current(null)}),g.on("state_update",_=>{d.current&&_&&typeof _=="object"&&d.current(_)}),g.connect(),()=>{g.disconnect(),f.current=null}},[]);const h=E.useCallback((g,_)=>{var S;return((S=f.current)==null?void 0:S.on(g,_))||(()=>{})},[]);return{connected:o,subscribe:h}}function fg(){const[i,o]=E.useState(""),{connected:r}=cg(()=>{});return E.useEffect(()=>{qa.getStatus().then(f=>{o(f.version||"")}).catch(()=>{})},[]),O.jsxs("div",{className:"flex h-screen bg-[#FAF9F6]",children:[O.jsx(ig,{}),O.jsx("a",{href:"#main-content",className:"sr-only focus:not-sr-only focus:absolute focus:z-50 focus:top-2 focus:left-2 focus:px-4 focus:py-2 focus:bg-white focus:text-[#553DE9] focus:rounded-[3px] focus:shadow-card",children:"Skip to main content"}),O.jsx(ng,{wsConnected:r,version:i}),O.jsx("main",{id:"main-content",className:"flex-1 overflow-auto",children:O.jsx(Cv,{})})]})}function _h({children:i}){const{user:o,loading:r,isLocalMode:f}=$h();return r?O.jsx("div",{className:"h-screen bg-[#FAF9F6] flex items-center justify-center text-[#6B6960] text-sm",children:"Loading..."}):f?O.jsx(O.Fragment,{children:i}):o?O.jsx(O.Fragment,{children:i}):O.jsx(Mv,{to:"/login",replace:!0})}function hi({variant:i="text",width:o,height:r,className:f=""}){const d="animate-pulse bg-[#ECEAE3]/60 rounded";if(i==="circle"){const h=o||"2rem";return O.jsx("div",{className:`${d} rounded-full flex-shrink-0 ${f}`,style:{width:h,height:h}})}return i==="block"?O.jsx("div",{className:`${d} rounded-btn ${f}`,style:{width:o||"100%",height:r||"4rem"}}):O.jsx("div",{className:`${d} rounded-btn ${f}`,style:{width:o||"100%",height:r||"0.75rem"}})}function bg(){return O.jsx("div",{className:"p-4 space-y-3",children:[...Array(12)].map((i,o)=>O.jsxs("div",{className:"flex items-center gap-3",children:[O.jsx(hi,{variant:"text",width:"1.5rem",height:"10px",className:"flex-shrink-0 opacity-40"}),O.jsx(hi,{variant:"text",width:`${20+Math.random()*60}%`,height:"10px"})]},o))})}const og=E.lazy(()=>Pl(()=>import("./HomePage-Dx4Ae0hu.js"),__vite__mapDeps([0,1,2,3]))),sg=E.lazy(()=>Pl(()=>import("./ProjectPage-BVnDGxXk.js"),__vite__mapDeps([4,3,5,2,6,7,8]))),rg=E.lazy(()=>Pl(()=>import("./ProjectsPage-2Fi6cKB-.js"),__vite__mapDeps([9,5,10,1,2]))),dg=E.lazy(()=>Pl(()=>import("./TemplatesPage-B-f1Gfbg.js"),__vite__mapDeps([11,10,1,2]))),hg=E.lazy(()=>Pl(()=>import("./SettingsPage-DOzGoyLv.js"),__vite__mapDeps([12,10,7]))),mg=E.lazy(()=>Pl(()=>import("./LoginPage-CRffqZNo.js"),[])),yg=E.lazy(()=>Pl(()=>import("./NotFoundPage-B1QZ92yR.js"),__vite__mapDeps([13,6])));function Il(){return O.jsxs("div",{className:"h-screen bg-[#FAF9F6] flex flex-col items-center justify-center gap-3",children:[O.jsx(hi,{variant:"block",width:"200px",height:"24px"}),O.jsx(hi,{variant:"text",width:"140px",height:"12px",className:"opacity-50"})]})}function vg(){return O.jsx(tg,{children:O.jsxs(Dv,{children:[O.jsx(il,{path:"/login",element:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(mg,{})})}),O.jsx(il,{path:"/project/:sessionId",element:O.jsx(_h,{children:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(sg,{})})})}),O.jsxs(il,{element:O.jsx(_h,{children:O.jsx(fg,{})}),children:[O.jsx(il,{path:"/",element:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(og,{})})}),O.jsx(il,{path:"/projects",element:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(rg,{})})}),O.jsx(il,{path:"/templates",element:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(dg,{})})}),O.jsx(il,{path:"/settings",element:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(hg,{})})}),O.jsx(il,{path:"*",element:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(yg,{})})})]})]})})}N0.createRoot(document.getElementById("root")).render(O.jsx(E.StrictMode,{children:O.jsx(l1,{children:O.jsx(vg,{})})}));export{S1 as A,E1 as B,_1 as E,R1 as F,N1 as H,Xh as L,Y1 as M,Vh as S,k1 as T,pg as W,Kh as X,qa as a,cg as b,Jt as c,bg as d,hi as e,Sg as f,$h as g,O as j,E as r,Ff as u};
|
|
186
|
+
*/const $1=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],Kh=Jt("x",$1),Jh=`${window.location.origin}/api`,W1=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws`;function F1(){const i={"Content-Type":"application/json"};try{const o=localStorage.getItem("pl_auth_token");o&&(i.Authorization=`Bearer ${o}`)}catch{}return i}async function W(i,o){const r=await fetch(`${Jh}${i}`,{...o,headers:{...F1(),...o==null?void 0:o.headers}});if(!r.ok){const f=await r.text().catch(()=>"");throw new Error(`API error ${r.status}: ${r.statusText}${f?` - ${f}`:""}`)}return r.json()}const qa={startSession:i=>W("/session/start",{method:"POST",body:JSON.stringify(i)}),stopSession:()=>W("/session/stop",{method:"POST"}),pauseSession:()=>W("/session/pause",{method:"POST"}),resumeSession:()=>W("/session/resume",{method:"POST"}),getPrdPrefill:()=>W("/session/prd-prefill"),getStatus:()=>W("/session/status"),getAgents:()=>W("/session/agents"),getLogs:(i=200)=>W(`/session/logs?lines=${i}`),getMemorySummary:()=>W("/session/memory"),getChecklist:()=>W("/session/checklist"),getFiles:()=>W("/session/files"),getFileContent:i=>W(`/session/files/content?path=${encodeURIComponent(i)}`),getSessionFileContent:(i,o)=>W(`/sessions/${encodeURIComponent(i)}/file?path=${encodeURIComponent(o)}`),getTemplates:()=>W("/templates"),getTemplateContent:i=>W(`/templates/${encodeURIComponent(i)}`),planSession:(i,o)=>W("/session/plan",{method:"POST",body:JSON.stringify({prd:i,provider:o})}),generateReport:(i="markdown")=>W("/session/report",{method:"POST",body:JSON.stringify({format:i})}),shareSession:()=>W("/session/share",{method:"POST"}),getCurrentProvider:()=>W("/provider/current"),setProvider:i=>W("/provider/set",{method:"POST",body:JSON.stringify({provider:i})}),getMetrics:()=>W("/session/metrics"),getSessionsHistory:()=>W("/sessions/history"),getSessionDetail:i=>W(`/sessions/${encodeURIComponent(i)}`),onboardRepo:i=>W("/session/onboard",{method:"POST",body:JSON.stringify({path:i})}),saveSessionFile:(i,o,r)=>W(`/sessions/${encodeURIComponent(i)}/file`,{method:"PUT",body:JSON.stringify({path:o,content:r})}),createSessionFile:(i,o,r="")=>W(`/sessions/${encodeURIComponent(i)}/file`,{method:"POST",body:JSON.stringify({path:o,content:r})}),deleteSessionFile:(i,o)=>W(`/sessions/${encodeURIComponent(i)}/file`,{method:"DELETE",body:JSON.stringify({path:o})}),createSessionDirectory:(i,o)=>W(`/sessions/${encodeURIComponent(i)}/directory`,{method:"POST",body:JSON.stringify({path:o})}),reviewProject:i=>W(`/sessions/${encodeURIComponent(i)}/review`,{method:"POST"}),testProject:i=>W(`/sessions/${encodeURIComponent(i)}/test`,{method:"POST"}),explainProject:i=>W(`/sessions/${encodeURIComponent(i)}/explain`,{method:"POST"}),exportProject:i=>W(`/sessions/${encodeURIComponent(i)}/export`,{method:"POST"}),chatMessage:(i,o,r="quick")=>W(`/sessions/${encodeURIComponent(i)}/chat`,{method:"POST",body:JSON.stringify({message:o,mode:r})}),chatStart:(i,o,r="quick")=>W(`/sessions/${encodeURIComponent(i)}/chat`,{method:"POST",body:JSON.stringify({message:o,mode:r})}),chatPoll:(i,o)=>W(`/sessions/${encodeURIComponent(i)}/chat/${encodeURIComponent(o)}`),chatStreamUrl:(i,o)=>`${Jh}/sessions/${encodeURIComponent(i)}/chat/${encodeURIComponent(o)}/stream`,chatCancel:(i,o)=>W(`/sessions/${encodeURIComponent(i)}/chat/${encodeURIComponent(o)}/cancel`,{method:"POST"}),getPreviewInfo:i=>W(`/sessions/${encodeURIComponent(i)}/preview-info`),devserver:{start:(i,o)=>W(`/sessions/${encodeURIComponent(i)}/devserver/start`,{method:"POST",body:JSON.stringify({command:o||null})}),stop:i=>W(`/sessions/${encodeURIComponent(i)}/devserver/stop`,{method:"POST"}),status:i=>W(`/sessions/${encodeURIComponent(i)}/devserver/status`)},getSecrets:()=>W("/secrets"),setSecret:(i,o)=>W("/secrets",{method:"POST",body:JSON.stringify({key:i,value:o})}),deleteSecret:i=>W(`/secrets/${encodeURIComponent(i)}`,{method:"DELETE"}),getMe:()=>W("/auth/me"),getGitHubAuthUrl:()=>W("/auth/github/url"),getGoogleAuthUrl:()=>W("/auth/google/url"),githubCallback:(i,o)=>W("/auth/github/callback",{method:"POST",body:JSON.stringify({code:i,state:o})}),googleCallback:(i,o,r)=>W("/auth/google/callback",{method:"POST",body:JSON.stringify({code:i,state:o,redirect_uri:r||`${window.location.origin}${window.location.pathname}`})})};class I1{constructor(o){Bn(this,"ws",null);Bn(this,"listeners",new Map);Bn(this,"reconnectTimer",null);Bn(this,"url");this.url=o||W1}connect(){var o;((o=this.ws)==null?void 0:o.readyState)!==WebSocket.OPEN&&(this.ws=new WebSocket(this.url),this.ws.onopen=()=>{this.emit("connected",{message:"WebSocket connected"})},this.ws.onmessage=r=>{try{const f=JSON.parse(r.data);this.emit(f.type,f.data||f)}catch{}},this.ws.onclose=()=>{this.emit("disconnected",{}),this.scheduleReconnect()},this.ws.onerror=()=>{var r;(r=this.ws)==null||r.close()})}scheduleReconnect(){this.reconnectTimer||(this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect()},3e3))}on(o,r){return this.listeners.has(o)||this.listeners.set(o,new Set),this.listeners.get(o).add(r),()=>{var f;return(f=this.listeners.get(o))==null?void 0:f.delete(r)}}emit(o,r){var f,d;(f=this.listeners.get(o))==null||f.forEach(h=>h(r)),(d=this.listeners.get("*"))==null||d.forEach(h=>h({type:o,data:r}))}send(o){var r;((r=this.ws)==null?void 0:r.readyState)===WebSocket.OPEN&&this.ws.send(JSON.stringify(o))}disconnect(){var o;this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),(o=this.ws)==null||o.close(),this.ws=null}}const Vf="pl_auth_token",kh=E.createContext({user:null,loading:!0,login:()=>{},logout:()=>{},isLocalMode:!0});function P1(i,o,r){o(!1),r({email:i.sub||i.email||"",name:i.name||"",avatar_url:i.avatar||"",authenticated:!0})}function tg({children:i}){const[o,r]=E.useState(null),[f,d]=E.useState(!0),[h,g]=E.useState(!0),_=Ff(),S=je();E.useEffect(()=>{let j=!1;async function w(){const Z=new URLSearchParams(window.location.search),Y=Z.get("token"),G=Z.get("code");if(Y)localStorage.setItem(Vf,Y),window.history.replaceState({},"",window.location.pathname);else if(G)try{const B=sessionStorage.getItem("pl_oauth_provider")||"github",F=sessionStorage.getItem("pl_oauth_state")||"";sessionStorage.removeItem("pl_oauth_state"),sessionStorage.removeItem("pl_oauth_provider");let K;if(B==="google"?K=await qa.googleCallback(G,F):K=await qa.githubCallback(G,F),!j){localStorage.setItem(Vf,K.token),window.history.replaceState({},"",window.location.pathname),g(!1),r({email:K.user.email,name:K.user.name,avatar_url:K.user.avatar_url,authenticated:!0}),d(!1);return}}catch{window.history.replaceState({},"",window.location.pathname)}try{const B=await qa.getMe();if(j)return;B.local_mode?(g(!0),r(null)):B.authenticated?P1(B,g,r):(g(!1),r(null))}catch{if(j)return;g(!0),r(null)}finally{j||d(!1)}}return w(),()=>{j=!0}},[]);const y=E.useCallback(j=>{j==="github"?qa.getGitHubAuthUrl().then(w=>{try{const Y=new URL(w.url).searchParams.get("state");Y&&(sessionStorage.setItem("pl_oauth_state",Y),sessionStorage.setItem("pl_oauth_provider","github"))}catch{}window.location.href=w.url}).catch(()=>{}):j==="google"&&qa.getGoogleAuthUrl().then(w=>{try{const Y=new URL(w.url).searchParams.get("state");Y&&(sessionStorage.setItem("pl_oauth_state",Y),sessionStorage.setItem("pl_oauth_provider","google"))}catch{}window.location.href=w.url}).catch(()=>{})},[]),M=E.useCallback(()=>{localStorage.removeItem(Vf),r(null),S.pathname!=="/login"&&_("/login",{replace:!0})},[_,S.pathname]),z=E.useMemo(()=>({user:o,loading:f,login:y,logout:M,isLocalMode:h}),[o,f,y,M,h]);return O.jsx(kh.Provider,{value:z,children:i})}function $h(){return E.useContext(kh)}const Th="pl_sidebar_collapsed",eg=[{to:"/",label:"Home",icon:N1},{to:"/projects",label:"Projects",icon:M1},{to:"/templates",label:"Templates",icon:U1}],lg=[{to:"/settings",label:"Settings",icon:Vh}];function ag(){const[i,o]=E.useState(typeof window<"u"?window.innerWidth<768:!1);return E.useEffect(()=>{const r=()=>o(window.innerWidth<768);return window.addEventListener("resize",r),()=>window.removeEventListener("resize",r)},[]),i}function ng({wsConnected:i,version:o}){const r=ag(),f=je(),[d,h]=E.useState(!1),[g,_]=E.useState(()=>{try{return localStorage.getItem(Th)==="1"}catch{return!1}});E.useEffect(()=>{try{localStorage.setItem(Th,g?"1":"0")}catch{}},[g]),E.useEffect(()=>{r&&h(!1)},[f.pathname,r]);const S=r?d:!g,y=S?240:64,M=j=>["flex items-center gap-3 px-3 py-2 text-sm transition-colors rounded-[5px]",!S&&"justify-center",j?"bg-[#553DE9]/8 text-[#553DE9] font-medium border-l-2 border-[#553DE9]":"text-[#36342E] hover:bg-[#F8F4F0]"].filter(Boolean).join(" "),z=O.jsxs("aside",{className:"flex flex-col h-full border-r border-[#ECEAE3] bg-white transition-[width] duration-200",style:{width:y,minWidth:y},children:[O.jsxs("div",{className:"flex items-center justify-between px-4 h-14 border-b border-[#ECEAE3]",children:[S&&O.jsxs("div",{className:"flex flex-col",children:[O.jsx("span",{className:"font-heading text-lg font-bold leading-tight text-[#36342E]",children:"Purple Lab"}),O.jsx("span",{className:"text-xs text-[#6B6960]",children:"Powered by Loki"})]}),r?O.jsx("button",{type:"button","aria-label":d?"Close menu":"Open menu",title:d?"Close menu":"Open menu",onClick:()=>h(!d),className:"inline-flex items-center justify-center w-7 h-7 rounded-[3px] text-[#939084] hover:bg-[#F8F4F0] transition-colors",children:d?O.jsx(Kh,{size:16}):O.jsx(L1,{size:16})}):O.jsx("button",{type:"button","aria-label":g?"Expand sidebar":"Collapse sidebar",title:g?"Expand sidebar":"Collapse sidebar",onClick:()=>_(!g),className:"inline-flex items-center justify-center w-7 h-7 rounded-[3px] text-[#939084] hover:bg-[#F8F4F0] transition-colors",children:g?O.jsx(V1,{size:16}):O.jsx(Q1,{size:16})})]}),O.jsxs("nav",{className:"flex-1 px-2 py-3 flex flex-col gap-1","aria-label":"Main navigation",children:[eg.map(j=>O.jsxs(di,{to:j.to,end:j.to==="/",className:({isActive:w})=>M(w),title:S?void 0:j.label,children:[O.jsx(j.icon,{size:18}),S&&O.jsx("span",{children:j.label})]},j.to)),O.jsx("div",{className:"my-2 border-t border-[#ECEAE3]"}),lg.map(j=>O.jsxs(di,{to:j.to,className:({isActive:w})=>M(w),title:S?void 0:j.label,children:[O.jsx(j.icon,{size:18}),S&&O.jsx("span",{children:j.label})]},j.to))]}),O.jsxs("div",{className:"px-3 py-3 border-t border-[#ECEAE3] flex flex-col gap-2",children:[O.jsx(ug,{collapsed:!S}),O.jsxs("div",{className:["flex items-center gap-2 text-xs",!S&&"justify-center"].filter(Boolean).join(" "),children:[O.jsx("span",{className:`w-2 h-2 rounded-full flex-shrink-0 ${i?"bg-[#1FC5A8]":"bg-[#C45B5B]"}`}),S&&O.jsx("span",{className:"text-[#6B6960]",children:i?"Connected":"Disconnected"})]}),S&&o&&O.jsxs("span",{className:"text-xs text-[#6B6960]",children:["v",o]}),O.jsxs("a",{href:"https://www.autonomi.dev/docs",target:"_blank",rel:"noopener noreferrer",className:["flex items-center gap-2 text-xs text-[#6B6960] hover:text-[#36342E] transition-colors",!S&&"justify-center"].filter(Boolean).join(" "),title:S?void 0:"Documentation",children:[O.jsx(E1,{size:14}),S&&O.jsx("span",{children:"Docs"})]})]})]});return r&&d?O.jsxs(O.Fragment,{children:[O.jsx("div",{className:"fixed inset-0 z-40 bg-ink/20",onClick:()=>h(!1)}),O.jsx("div",{className:"fixed inset-y-0 left-0 z-50",children:z})]}):z}function ug({collapsed:i}){const{user:o,logout:r,isLocalMode:f}=$h(),[d,h]=E.useState(!1),g=E.useRef(null);return E.useEffect(()=>{function _(S){g.current&&!g.current.contains(S.target)&&h(!1)}if(d)return document.addEventListener("mousedown",_),()=>document.removeEventListener("mousedown",_)},[d]),f||!o?O.jsxs("div",{className:["flex items-center gap-2 text-xs",i&&"justify-center"].filter(Boolean).join(" "),title:i?"Local Mode":void 0,children:[O.jsx(G1,{size:14,className:"text-[#939084] flex-shrink-0"}),!i&&O.jsx("span",{className:"text-[#939084]",children:"Local Mode"})]}):O.jsxs("div",{className:"relative",ref:g,"data-testid":"user-section",children:[O.jsxs("button",{type:"button",onClick:()=>h(!d),className:["flex items-center gap-2 w-full text-left text-xs rounded-[3px] py-1 px-1 hover:bg-[#F8F4F0] transition-colors",i&&"justify-center"].filter(Boolean).join(" "),title:i?o.name||o.email:void 0,children:[o.avatar_url?O.jsx("img",{src:o.avatar_url,alt:"",className:"w-5 h-5 rounded-full flex-shrink-0"}):O.jsx("div",{className:"w-5 h-5 rounded-full bg-[#553DE9] flex items-center justify-center text-white text-[10px] font-bold flex-shrink-0",children:(o.name||o.email||"?")[0].toUpperCase()}),!i&&O.jsxs(O.Fragment,{children:[O.jsx("span",{className:"text-[#36342E] truncate flex-1",children:o.name||o.email}),O.jsx(T1,{size:12,className:`text-[#939084] transition-transform ${d?"":"rotate-180"}`})]})]}),d&&O.jsxs("div",{className:"absolute bottom-full left-0 mb-1 w-48 bg-white border border-[#ECEAE3] rounded-lg shadow-lg py-1 z-50",children:[O.jsxs("div",{className:"px-3 py-2 border-b border-[#ECEAE3]",children:[O.jsx("p",{className:"text-xs font-medium text-[#36342E] truncate",children:o.name}),O.jsx("p",{className:"text-xs text-[#939084] truncate",children:o.email})]}),O.jsxs(di,{to:"/settings",onClick:()=>h(!1),className:"flex items-center gap-2 px-3 py-2 text-xs text-[#36342E] hover:bg-[#F8F4F0] transition-colors",children:[O.jsx(Vh,{size:14}),"Settings"]}),O.jsxs("button",{type:"button",onClick:()=>{h(!1),r()},className:"flex items-center gap-2 px-3 py-2 text-xs text-[#C45B5B] hover:bg-[#F8F4F0] transition-colors w-full text-left",children:[O.jsx(H1,{size:14}),"Sign out"]})]})]})}const zh="pl_onboarding_complete",wn=[{icon:R1,title:"Write your PRD",description:"Describe what you want to build, or choose a template to get started quickly."},{icon:k1,title:"Use the terminal",description:"Run commands directly in the integrated terminal to install dependencies or debug."},{icon:_1,title:"Preview in real-time",description:"Switch to the Preview tab to see your app running with live reload."},{icon:Y1,title:"Iterate with AI Chat",description:"Ask the AI to modify, fix, or explain your code in the chat panel."}];function ig(){const[i,o]=E.useState(!1),[r,f]=E.useState(0);E.useEffect(()=>{try{localStorage.getItem(zh)!=="1"&&o(!0)}catch{}},[]);const d=()=>{o(!1);try{localStorage.setItem(zh,"1")}catch{}},h=()=>{r<wn.length-1?f(r+1):d()};if(!i)return null;const g=wn[r],_=g.icon;return O.jsx("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-ink/30",children:O.jsxs("div",{className:"bg-card rounded-card shadow-card-hover border border-border w-full max-w-sm mx-4",children:[O.jsxs("div",{className:"flex items-center justify-between px-5 pt-5 pb-2",children:[O.jsxs("span",{className:"text-[11px] font-mono text-muted-accessible",children:[r+1," / ",wn.length]}),O.jsx("button",{onClick:d,className:"text-muted hover:text-ink transition-colors p-1 rounded-btn hover:bg-hover",title:"Skip onboarding",children:O.jsx(Kh,{size:14})})]}),O.jsxs("div",{className:"px-5 pb-4 text-center",children:[O.jsx("div",{className:"w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-4",children:O.jsx(_,{size:24,className:"text-primary"})}),O.jsx("h3",{className:"text-sm font-heading font-bold text-ink mb-1",children:g.title}),O.jsx("p",{className:"text-xs text-muted-accessible leading-relaxed",children:g.description})]}),O.jsx("div",{className:"flex items-center justify-center gap-1.5 pb-4",children:wn.map((S,y)=>O.jsx("div",{className:`w-1.5 h-1.5 rounded-full transition-colors ${y===r?"bg-primary":"bg-border"}`},y))}),O.jsxs("div",{className:"flex items-center justify-between px-5 py-3 border-t border-border",children:[O.jsx("button",{onClick:d,className:"text-xs text-muted hover:text-ink transition-colors",children:"Skip"}),O.jsx("button",{onClick:h,className:"inline-flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium rounded-btn bg-primary text-white hover:bg-primary-hover transition-colors",children:r<wn.length-1?O.jsxs(O.Fragment,{children:["Next",O.jsx(S1,{size:12})]}):"Get Started"})]})]})})}function cg(i){const[o,r]=E.useState(!1),f=E.useRef(null),d=E.useRef(i);E.useEffect(()=>{d.current=i},[i]),E.useEffect(()=>{const g=new I1;return f.current=g,g.on("connected",()=>r(!0)),g.on("disconnected",()=>{r(!1),d.current&&d.current(null)}),g.on("state_update",_=>{d.current&&_&&typeof _=="object"&&d.current(_)}),g.connect(),()=>{g.disconnect(),f.current=null}},[]);const h=E.useCallback((g,_)=>{var S;return((S=f.current)==null?void 0:S.on(g,_))||(()=>{})},[]);return{connected:o,subscribe:h}}function fg(){const[i,o]=E.useState(""),{connected:r}=cg(()=>{});return E.useEffect(()=>{qa.getStatus().then(f=>{o(f.version||"")}).catch(()=>{})},[]),O.jsxs("div",{className:"flex h-screen bg-[#FAF9F6]",children:[O.jsx(ig,{}),O.jsx("a",{href:"#main-content",className:"sr-only focus:not-sr-only focus:absolute focus:z-50 focus:top-2 focus:left-2 focus:px-4 focus:py-2 focus:bg-white focus:text-[#553DE9] focus:rounded-[3px] focus:shadow-card",children:"Skip to main content"}),O.jsx(ng,{wsConnected:r,version:i}),O.jsx("main",{id:"main-content",className:"flex-1 overflow-auto",children:O.jsx(Cv,{})})]})}function _h({children:i}){const{user:o,loading:r,isLocalMode:f}=$h();return r?O.jsx("div",{className:"h-screen bg-[#FAF9F6] flex items-center justify-center text-[#6B6960] text-sm",children:"Loading..."}):f?O.jsx(O.Fragment,{children:i}):o?O.jsx(O.Fragment,{children:i}):O.jsx(Mv,{to:"/login",replace:!0})}function hi({variant:i="text",width:o,height:r,className:f=""}){const d="animate-pulse bg-[#ECEAE3]/60 rounded";if(i==="circle"){const h=o||"2rem";return O.jsx("div",{className:`${d} rounded-full flex-shrink-0 ${f}`,style:{width:h,height:h}})}return i==="block"?O.jsx("div",{className:`${d} rounded-btn ${f}`,style:{width:o||"100%",height:r||"4rem"}}):O.jsx("div",{className:`${d} rounded-btn ${f}`,style:{width:o||"100%",height:r||"0.75rem"}})}function bg(){return O.jsx("div",{className:"p-4 space-y-3",children:[...Array(12)].map((i,o)=>O.jsxs("div",{className:"flex items-center gap-3",children:[O.jsx(hi,{variant:"text",width:"1.5rem",height:"10px",className:"flex-shrink-0 opacity-40"}),O.jsx(hi,{variant:"text",width:`${20+Math.random()*60}%`,height:"10px"})]},o))})}const og=E.lazy(()=>Pl(()=>import("./HomePage-B8kMCXMB.js"),__vite__mapDeps([0,1,2,3]))),sg=E.lazy(()=>Pl(()=>import("./ProjectPage-C-k0iy0i.js"),__vite__mapDeps([4,3,5,2,6,7,8]))),rg=E.lazy(()=>Pl(()=>import("./ProjectsPage-jys_pHzp.js"),__vite__mapDeps([9,5,10,1,2]))),dg=E.lazy(()=>Pl(()=>import("./TemplatesPage-COnhb_Wq.js"),__vite__mapDeps([11,10,1,2]))),hg=E.lazy(()=>Pl(()=>import("./SettingsPage-Cz_RXr82.js"),__vite__mapDeps([12,10,7]))),mg=E.lazy(()=>Pl(()=>import("./LoginPage-D9lCyiqM.js"),[])),yg=E.lazy(()=>Pl(()=>import("./NotFoundPage-DzeZ0uQ6.js"),__vite__mapDeps([13,6])));function Il(){return O.jsxs("div",{className:"h-screen bg-[#FAF9F6] flex flex-col items-center justify-center gap-3",children:[O.jsx(hi,{variant:"block",width:"200px",height:"24px"}),O.jsx(hi,{variant:"text",width:"140px",height:"12px",className:"opacity-50"})]})}function vg(){return O.jsx(tg,{children:O.jsxs(Dv,{children:[O.jsx(il,{path:"/login",element:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(mg,{})})}),O.jsx(il,{path:"/project/:sessionId",element:O.jsx(_h,{children:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(sg,{})})})}),O.jsxs(il,{element:O.jsx(_h,{children:O.jsx(fg,{})}),children:[O.jsx(il,{path:"/",element:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(og,{})})}),O.jsx(il,{path:"/projects",element:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(rg,{})})}),O.jsx(il,{path:"/templates",element:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(dg,{})})}),O.jsx(il,{path:"/settings",element:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(hg,{})})}),O.jsx(il,{path:"*",element:O.jsx(E.Suspense,{fallback:O.jsx(Il,{}),children:O.jsx(yg,{})})})]})]})})}N0.createRoot(document.getElementById("root")).render(O.jsx(E.StrictMode,{children:O.jsx(l1,{children:O.jsx(vg,{})})}));export{S1 as A,E1 as B,_1 as E,R1 as F,N1 as H,Xh as L,Y1 as M,Vh as S,k1 as T,pg as W,Kh as X,qa as a,cg as b,Jt as c,bg as d,hi as e,Sg as f,$h as g,O as j,E as r,Ff as u};
|
package/web-app/dist/index.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-B8Eg1YHL.js"></script>
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/assets/index-UNfgZjJl.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body class="bg-background text-ink font-sans antialiased">
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Purple Lab v2 - Full Stack Development
|
|
2
|
+
# Usage: docker compose -f docker-compose.purple-lab.yml up
|
|
3
|
+
|
|
4
|
+
services:
|
|
5
|
+
purple-lab:
|
|
6
|
+
build:
|
|
7
|
+
context: .
|
|
8
|
+
dockerfile: Dockerfile
|
|
9
|
+
ports:
|
|
10
|
+
- "57375:57375"
|
|
11
|
+
environment:
|
|
12
|
+
- PURPLE_LAB_HOST=0.0.0.0
|
|
13
|
+
- DATABASE_URL=postgresql+asyncpg://purplelab:purplelab@postgres:5432/purplelab
|
|
14
|
+
- PURPLE_LAB_SECRET_KEY=${PURPLE_LAB_SECRET_KEY:-dev-secret-change-in-production}
|
|
15
|
+
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-}
|
|
16
|
+
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-}
|
|
17
|
+
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
|
|
18
|
+
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
|
|
19
|
+
volumes:
|
|
20
|
+
- project-data:/projects
|
|
21
|
+
- loki-data:/home/purplelab/.loki
|
|
22
|
+
depends_on:
|
|
23
|
+
postgres:
|
|
24
|
+
condition: service_healthy
|
|
25
|
+
healthcheck:
|
|
26
|
+
test: ["CMD", "curl", "-f", "http://localhost:57375/health"]
|
|
27
|
+
interval: 10s
|
|
28
|
+
timeout: 5s
|
|
29
|
+
retries: 3
|
|
30
|
+
start_period: 15s
|
|
31
|
+
restart: unless-stopped
|
|
32
|
+
networks:
|
|
33
|
+
- purple-lab
|
|
34
|
+
|
|
35
|
+
postgres:
|
|
36
|
+
image: postgres:16-alpine
|
|
37
|
+
environment:
|
|
38
|
+
- POSTGRES_USER=purplelab
|
|
39
|
+
- POSTGRES_PASSWORD=purplelab
|
|
40
|
+
- POSTGRES_DB=purplelab
|
|
41
|
+
volumes:
|
|
42
|
+
- postgres-data:/var/lib/postgresql/data
|
|
43
|
+
ports:
|
|
44
|
+
- "5433:5432" # Avoid conflict with host postgres
|
|
45
|
+
healthcheck:
|
|
46
|
+
test: ["CMD-SHELL", "pg_isready -U purplelab"]
|
|
47
|
+
interval: 5s
|
|
48
|
+
timeout: 3s
|
|
49
|
+
retries: 5
|
|
50
|
+
restart: unless-stopped
|
|
51
|
+
networks:
|
|
52
|
+
- purple-lab
|
|
53
|
+
|
|
54
|
+
redis:
|
|
55
|
+
image: redis:7-alpine
|
|
56
|
+
ports:
|
|
57
|
+
- "6380:6379" # Avoid conflict with host redis
|
|
58
|
+
healthcheck:
|
|
59
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
60
|
+
interval: 5s
|
|
61
|
+
timeout: 3s
|
|
62
|
+
retries: 5
|
|
63
|
+
restart: unless-stopped
|
|
64
|
+
profiles:
|
|
65
|
+
- full # Only start with --profile full
|
|
66
|
+
networks:
|
|
67
|
+
- purple-lab
|
|
68
|
+
|
|
69
|
+
volumes:
|
|
70
|
+
project-data:
|
|
71
|
+
loki-data:
|
|
72
|
+
postgres-data:
|
|
73
|
+
|
|
74
|
+
networks:
|
|
75
|
+
purple-lab:
|
|
76
|
+
driver: bridge
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Alembic environment configuration for Purple Lab.
|
|
2
|
+
|
|
3
|
+
Supports both sync and async migration modes. Reads the database URL
|
|
4
|
+
from the DATABASE_URL environment variable at runtime, overriding any
|
|
5
|
+
value in alembic.ini.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from logging.config import fileConfig
|
|
10
|
+
|
|
11
|
+
from alembic import context
|
|
12
|
+
from sqlalchemy import engine_from_config, pool, text
|
|
13
|
+
from sqlalchemy.engine import Connection
|
|
14
|
+
|
|
15
|
+
# Ensure the web-app package root is importable so we can reference models.
|
|
16
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
17
|
+
|
|
18
|
+
from models import Base # noqa: E402
|
|
19
|
+
|
|
20
|
+
# Alembic Config object -- provides access to alembic.ini values.
|
|
21
|
+
config = context.config
|
|
22
|
+
|
|
23
|
+
# Set up Python logging from the ini file.
|
|
24
|
+
if config.config_file_name is not None:
|
|
25
|
+
fileConfig(config.config_file_name)
|
|
26
|
+
|
|
27
|
+
# Target metadata for autogenerate support.
|
|
28
|
+
target_metadata = Base.metadata
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_url() -> str:
|
|
32
|
+
"""Return the database URL from the environment.
|
|
33
|
+
|
|
34
|
+
Falls back to the value in alembic.ini if DATABASE_URL is not set,
|
|
35
|
+
which will intentionally fail so that migrations are never run against
|
|
36
|
+
an unconfigured placeholder.
|
|
37
|
+
"""
|
|
38
|
+
url = os.environ.get("DATABASE_URL")
|
|
39
|
+
if url:
|
|
40
|
+
# asyncpg:// -> postgresql+asyncpg:// normalisation
|
|
41
|
+
if url.startswith("postgres://"):
|
|
42
|
+
url = url.replace("postgres://", "postgresql+asyncpg://", 1)
|
|
43
|
+
elif url.startswith("postgresql://"):
|
|
44
|
+
url = url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
|
45
|
+
return url
|
|
46
|
+
return config.get_main_option("sqlalchemy.url", "")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run_migrations_offline() -> None:
|
|
50
|
+
"""Run migrations in 'offline' mode.
|
|
51
|
+
|
|
52
|
+
Generates SQL scripts without connecting to the database.
|
|
53
|
+
"""
|
|
54
|
+
url = get_url()
|
|
55
|
+
context.configure(
|
|
56
|
+
url=url,
|
|
57
|
+
target_metadata=target_metadata,
|
|
58
|
+
literal_binds=True,
|
|
59
|
+
dialect_opts={"paramstyle": "named"},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
with context.begin_transaction():
|
|
63
|
+
context.run_migrations()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def do_run_migrations(connection: Connection) -> None:
|
|
67
|
+
"""Shared helper that configures the context and runs migrations."""
|
|
68
|
+
context.configure(connection=connection, target_metadata=target_metadata)
|
|
69
|
+
with context.begin_transaction():
|
|
70
|
+
context.run_migrations()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_migrations_online() -> None:
|
|
74
|
+
"""Run migrations in 'online' mode using a sync connection.
|
|
75
|
+
|
|
76
|
+
For async drivers (asyncpg), Alembic still uses a synchronous
|
|
77
|
+
connection under the hood via ``run_sync``. If the URL contains
|
|
78
|
+
``+asyncpg``, we swap to the sync ``psycopg2`` driver for the
|
|
79
|
+
migration connection so that plain ``engine_from_config`` works.
|
|
80
|
+
"""
|
|
81
|
+
url = get_url()
|
|
82
|
+
|
|
83
|
+
# For migration purposes, use a sync driver so Alembic's default
|
|
84
|
+
# engine_from_config works without needing the async runner.
|
|
85
|
+
sync_url = url.replace("+asyncpg", "+psycopg2") if "+asyncpg" in url else url
|
|
86
|
+
|
|
87
|
+
configuration = config.get_section(config.config_ini_section, {})
|
|
88
|
+
configuration["sqlalchemy.url"] = sync_url
|
|
89
|
+
|
|
90
|
+
connectable = engine_from_config(
|
|
91
|
+
configuration,
|
|
92
|
+
prefix="sqlalchemy.",
|
|
93
|
+
poolclass=pool.NullPool,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
with connectable.connect() as connection:
|
|
97
|
+
do_run_migrations(connection)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if context.is_offline_mode():
|
|
101
|
+
run_migrations_offline()
|
|
102
|
+
else:
|
|
103
|
+
run_migrations_online()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
"""
|
|
7
|
+
from typing import Sequence, Union
|
|
8
|
+
|
|
9
|
+
from alembic import op
|
|
10
|
+
import sqlalchemy as sa
|
|
11
|
+
${imports if imports else ""}
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision: str = ${repr(up_revision)}
|
|
15
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
16
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
17
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade() -> None:
|
|
21
|
+
${upgrades if upgrades else "pass"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def downgrade() -> None:
|
|
25
|
+
${downgrades if downgrades else "pass"}
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Initial schema -- users, projects, sessions, secrets, audit_log
|
|
2
|
+
|
|
3
|
+
Revision ID: 001
|
|
4
|
+
Revises: None
|
|
5
|
+
Create Date: 2026-03-21
|
|
6
|
+
"""
|
|
7
|
+
from typing import Sequence, Union
|
|
8
|
+
|
|
9
|
+
from alembic import op
|
|
10
|
+
import sqlalchemy as sa
|
|
11
|
+
from sqlalchemy.dialects.postgresql import UUID
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision: str = "001"
|
|
15
|
+
down_revision: Union[str, None] = None
|
|
16
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
17
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade() -> None:
|
|
21
|
+
# -- users --
|
|
22
|
+
op.create_table(
|
|
23
|
+
"users",
|
|
24
|
+
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
|
25
|
+
sa.Column("email", sa.String(255), unique=True, nullable=False, index=True),
|
|
26
|
+
sa.Column("name", sa.String(255)),
|
|
27
|
+
sa.Column("avatar_url", sa.String(500)),
|
|
28
|
+
sa.Column("provider", sa.String(50)),
|
|
29
|
+
sa.Column("provider_id", sa.String(255)),
|
|
30
|
+
sa.Column("password_hash", sa.String(255), nullable=True),
|
|
31
|
+
sa.Column("created_at", sa.DateTime),
|
|
32
|
+
sa.Column("last_login", sa.DateTime),
|
|
33
|
+
sa.Column("is_active", sa.Boolean, server_default=sa.text("true")),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# -- projects --
|
|
37
|
+
op.create_table(
|
|
38
|
+
"projects",
|
|
39
|
+
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
|
40
|
+
sa.Column(
|
|
41
|
+
"user_id",
|
|
42
|
+
UUID(as_uuid=True),
|
|
43
|
+
sa.ForeignKey("users.id"),
|
|
44
|
+
nullable=False,
|
|
45
|
+
),
|
|
46
|
+
sa.Column("name", sa.String(255), nullable=False),
|
|
47
|
+
sa.Column("description", sa.Text),
|
|
48
|
+
sa.Column("project_dir", sa.String(500), nullable=False),
|
|
49
|
+
sa.Column("created_at", sa.DateTime),
|
|
50
|
+
sa.Column("updated_at", sa.DateTime),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# -- sessions --
|
|
54
|
+
op.create_table(
|
|
55
|
+
"sessions",
|
|
56
|
+
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
|
57
|
+
sa.Column(
|
|
58
|
+
"user_id",
|
|
59
|
+
UUID(as_uuid=True),
|
|
60
|
+
sa.ForeignKey("users.id"),
|
|
61
|
+
nullable=False,
|
|
62
|
+
),
|
|
63
|
+
sa.Column(
|
|
64
|
+
"project_id",
|
|
65
|
+
UUID(as_uuid=True),
|
|
66
|
+
sa.ForeignKey("projects.id"),
|
|
67
|
+
nullable=True,
|
|
68
|
+
),
|
|
69
|
+
sa.Column("prd_content", sa.Text),
|
|
70
|
+
sa.Column("provider", sa.String(50), server_default="claude"),
|
|
71
|
+
sa.Column("mode", sa.String(50), server_default="standard"),
|
|
72
|
+
sa.Column("status", sa.String(50), server_default="created"),
|
|
73
|
+
sa.Column("started_at", sa.DateTime),
|
|
74
|
+
sa.Column("ended_at", sa.DateTime, nullable=True),
|
|
75
|
+
sa.Column("metadata_json", sa.JSON),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# -- secrets --
|
|
79
|
+
op.create_table(
|
|
80
|
+
"secrets",
|
|
81
|
+
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
|
82
|
+
sa.Column(
|
|
83
|
+
"user_id",
|
|
84
|
+
UUID(as_uuid=True),
|
|
85
|
+
sa.ForeignKey("users.id"),
|
|
86
|
+
nullable=False,
|
|
87
|
+
),
|
|
88
|
+
sa.Column("key", sa.String(255), nullable=False),
|
|
89
|
+
sa.Column("encrypted_value", sa.Text, nullable=False),
|
|
90
|
+
sa.Column("created_at", sa.DateTime),
|
|
91
|
+
sa.Column("updated_at", sa.DateTime),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# -- audit_log --
|
|
95
|
+
op.create_table(
|
|
96
|
+
"audit_log",
|
|
97
|
+
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
|
98
|
+
sa.Column(
|
|
99
|
+
"user_id",
|
|
100
|
+
UUID(as_uuid=True),
|
|
101
|
+
sa.ForeignKey("users.id"),
|
|
102
|
+
nullable=True,
|
|
103
|
+
),
|
|
104
|
+
sa.Column("action", sa.String(100), nullable=False),
|
|
105
|
+
sa.Column("resource_type", sa.String(50)),
|
|
106
|
+
sa.Column("resource_id", sa.String(255)),
|
|
107
|
+
sa.Column("details", sa.JSON),
|
|
108
|
+
sa.Column("ip_address", sa.String(45)),
|
|
109
|
+
sa.Column("created_at", sa.DateTime),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def downgrade() -> None:
|
|
114
|
+
op.drop_table("audit_log")
|
|
115
|
+
op.drop_table("secrets")
|
|
116
|
+
op.drop_table("sessions")
|
|
117
|
+
op.drop_table("projects")
|
|
118
|
+
op.drop_table("users")
|