@mistflow-ai/mcp 1.0.9 → 1.0.10

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.
Files changed (3) hide show
  1. package/dist/cli.js +14 -18
  2. package/dist/index.js +14 -18
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2228,16 +2228,12 @@ ${o}
2228
2228
  </html>
2229
2229
  `}var to="__mistflow_url_choice__",Ua=600*1e3;function Mn(){let t=le(Le(),".mistflow","confirm-secret");if(Ct(t))try{return Buffer.from(jn(t,"utf-8").trim(),"hex")}catch{}let e=Ma(32);return oo(le(Le(),".mistflow"),{recursive:!0}),ro(t,e.toString("hex"),{mode:384}),e}function Ln(t){return Oa("sha256").update(t.trim().toLowerCase()).digest("hex").slice(0,16)}function $a(t,e){let r={cwd:t,d:Ln(e),exp:Date.now()+Ua},o=Buffer.from(JSON.stringify(r)).toString("base64url"),n=On("sha256",Mn()).update(o).digest("base64url");return`${o}.${n}`}function Fa(t,e,r){let o=t.split(".");if(o.length!==2)return!1;let[n,i]=o,s=On("sha256",Mn()).update(n).digest("base64url"),a=Buffer.from(i),l=Buffer.from(s);if(a.length!==l.length||!La(a,l))return!1;try{let d=JSON.parse(Buffer.from(n,"base64url").toString("utf-8"));return!(typeof d.exp!="number"||Date.now()>d.exp||d.cwd!==e||d.d!==Ln(r))}catch{return!1}}function qa(t){let e=t,r=Le(),o=!1;for(let n=0;n<64;n++){if(Ct(le(e,"mistflow.json")))return"mistflow";if(!o&&Ct(le(e,"package.json"))&&(o=!0),e===r)break;let i=Da(e);if(i===e)break;e=i}return o?"foreign":"none"}function Ba(t){let e=Le(),r=t.replace(/\/+$/,"");if(r===e||r==="/"||r===""||r==="/tmp"||r==="/private/tmp")return!0;let o=["Desktop","Documents","Downloads"];for(let n of o)if(r===le(e,n))return!0;return!1}var za=C.object({description:C.string().optional().describe("App description or modification request. Required for the first call; omit on follow-up polls where only conversationId is passed. "),projectPath:C.string().min(1).describe("REQUIRED. Absolute path to the user's current working directory \u2014 where the Mistflow app will be scaffolded. Pass the directory the user is actually working in (e.g. /Users/alice/projects). Do NOT pass '/', '~', $HOME, Desktop, Documents, Downloads, or /tmp \u2014 the tool will refuse to scaffold at those locations. If you are unsure of the user's working directory, ask them before calling this tool."),conversationId:C.string().optional().describe("Returned by a previous mist_plan call with status 'clarify' or 'running'. Pass it back alone (no description) to poll an in-flight discovery call; pass with answers to submit responses."),answers:C.union([C.record(C.string()),C.array(C.object({question:C.string().optional(),decisionKey:C.string().optional(),answer:C.string()}))]).optional().describe("User's answers to the clarifying questions. Preferred shape: array of { question, decisionKey, answer } objects (supports duplicate decisionKeys). Legacy shape: { '<question text>': '<answer label>' } object map. Both are accepted; the server normalizes either."),existingPlan:C.record(C.unknown()).optional().describe("If provided, modifies this existing plan instead of creating a new one. Pass the current plan object from mistflow.json."),existingPlanId:C.string().optional().describe("Alternative to existingPlan \u2014 pass the planId from a previous mist_plan call to modify that plan."),templateToken:C.string().optional().describe("Fork from a shared template. Pass the share token (from a mistflow.ai/t/... URL) to clone that project's plan into your workspace."),remixDescription:C.string().optional().describe("Optional remix request when forking a template. Describes how you want the template to be different. E.g. 'Make it for tracking books instead of habits, add a search feature.' Only used with templateToken."),autonomous:C.boolean().optional().describe("DANGER \u2014 only set this to `true` when the user has EXPLICITLY asked to skip discovery questions (phrases like 'no questions', 'just build it', 'autonomous mode', 'don't ask'). Default is `false` and should stay that way in every other case. Do NOT set autonomous=true as a retry strategy after a plan-gen failure \u2014 it doesn't make things more reliable, it just removes the user's control over discovery answers (roles, integrations, auth model, etc.) and Sonnet ends up guessing. If a plan call fails, retry the SAME call with the same parameters, not a more aggressive one. Do NOT set autonomous=true to speed things up \u2014 the server already runs plan-gen in the background and polls, so there is no speed benefit. When set, skips the clarify round and generates the plan straight from the description. The user loses the chance to pick any option."),landingDesign:C.string().optional().describe("ID of a curated landing page design for the hero section. When set, the design's detailed blueprint (colors, fonts, layout, animations) is injected during the landing page implementation step. Use mist_project with action='landing-designs' to browse available landing designs."),appStyle:C.string().optional().describe("ID of a full-app style (e.g. 'stripe', 'linear', 'vercel', 'notion'). When set, the style's color palette, typography, component specs, shadows, and layout rules are injected during ALL implementation steps for consistent brand-quality design across every page. Use mist_project with action='app-styles' to browse available app styles."),language:C.string().optional().describe("UI language for the app. All user-facing text, labels, buttons, and content will be generated in this language. Use the language name in English (e.g. 'Spanish', 'French', 'Arabic', 'Japanese'). Defaults to English if not specified."),brandMentioned:C.boolean().optional().describe("Set to true ONLY when the user's original request explicitly invoked Mistflow by name (e.g. 'build me a CRM using mist', 'make a todo app with mistflow'). Skips the existing-project confirmation gate because the user clearly wants Mistflow. Do NOT set this for generic 'build me X' requests. Do NOT infer this \u2014 only set it when the user literally typed 'mist' or 'mistflow'."),confirmToken:C.string().optional().describe("The token returned in a previous mist_plan response with status 'confirm_new_project'. Only pass this AFTER asking the user via AskUserQuestion and they chose to scaffold a new Mistflow app in an existing-project directory. The token is bound to the projectPath and description \u2014 you must pass the SAME description AND projectPath on the retry."),urlChoice:C.string().optional().describe("The user's answer to the 'Your app URL' question from a previous mist_plan response. Pass JUST the subdomain (e.g. 'nutrition-tracker'), not the full URL or the option label. If the user kept the default suggestion, pass the suggested subdomain verbatim. If they typed a custom URL like 'myapp.mistflow.app', pass just 'myapp'. Pass this as a top-level parameter \u2014 do NOT nest it inside answers. The answers-dict magic key is deprecated and unreliable."),designConversationId:C.string().optional().describe("Returned by a previous mist_plan call with status 'design_clarify'. Pass it back on the follow-up call together with designDirection to finalize the plan's DESIGN.md."),designDirection:C.object({id:C.string().optional(),name:C.string().optional(),summary:C.string().optional(),heroHeadline:C.string().optional(),ctaText:C.string().optional(),bodySample:C.string().optional(),fontsHint:C.string().optional(),fonts:C.object({display:C.string(),body:C.string()}).partial().optional(),colorMood:C.string().optional(),colors:C.object({bg:C.string(),fg:C.string(),accent:C.string()}).partial().optional(),heroTreatment:C.string().optional(),shapeLang:C.string().optional(),texture:C.string().optional(),decorationHint:C.string().optional(),custom:C.string().optional()}).passthrough().optional().describe("The creative direction the user picked from a previous 'design_clarify' response. Pass the FULL direction object the user chose (all fields from the 'directions' array). If the user wrote their own description instead of picking one, pass { custom: '<their description>' } and omit the other fields.")});function Ha(t){let e=[[/payment/i,"Payments"],[/database/i,"Database"],[/auth|sign.?up|login|access/i,"Access"],[/landing.?page/i,"Landing page"],[/who.*using|user|role/i,"Users"],[/design|theme|style/i,"Design"],[/deploy/i,"Deploy"],[/domain/i,"Domain"],[/notification/i,"Notify"],[/email/i,"Email"],[/mobile|responsive/i,"Mobile"],[/integrat/i,"Integration"],[/field|info|propert|detail|contain/i,"Item shape"],[/view|layout|board|grid|list|timeline/i,"View"],[/scope|how many|one.*or.*many|multi/i,"Scope"],[/share|read.?only|viewer|stakeholder/i,"Sharing"],[/workflow|status|state|move|stage|pipeline/i,"Workflow"],[/avoid|bloat|simple|complex|minimal/i,"Constraints"],[/time.*period|quarter|month|sprint/i,"Time periods"],[/swimlane|column|group|categor/i,"Structure"]];for(let[o,n]of e)if(o.test(t))return n;return t.replace(/[?.,!]/g,"").split(/\s+/).filter(o=>!["what","how","do","does","is","are","the","a","an","would","should","you","your","for","this","that","to","of","or","and","want","like","prefer"].includes(o.toLowerCase())).slice(0,2).join(" ").slice(0,12)||"Option"}function Wa(t){let e=le(Le(),".mistflow","plans",`${t}.json`);if(!Ct(e))return null;try{return JSON.parse(jn(e,"utf-8")).plan??null}catch{return null}}async function Ga(t){for(let o=0;o<60;o++){try{let n=await No(t.design_conversation_id);if(n.status==="ready")return{status:"design_clarify",design_conversation_id:t.design_conversation_id,directions:n.directions,plan:n.plan,methodology:n.methodology};if(n.status==="failed")return{status:"ready",plan:n.plan,methodology:n.methodology}}catch(n){let i=n instanceof Error?n.message:String(n);if(i.toLowerCase().includes("not found"))return{status:"ready",plan:t.plan,methodology:t.methodology};console.error(`[plan] directions poll attempt ${o+1} failed: ${i}`)}await new Promise(n=>setTimeout(n,2e3))}return console.error("[plan] directions poll exhausted, falling back to ready"),{status:"ready",plan:t.plan,methodology:t.methodology}}async function Va(t,e){let{description:r,projectPath:o,conversationId:n,answers:i,existingPlan:s,existingPlanId:a,templateToken:l,remixDescription:d,autonomous:h,language:m,landingDesign:p,appStyle:y,brandMentioned:x,confirmToken:b,urlChoice:f,designConversationId:S,designDirection:A}=t;if(n&&!r&&!i&&!S&&!A&&!s&&!a&&!l)try{let u=await Bt(n);return u.status==="clarify_pending"?c(JSON.stringify({status:"running",conversationId:n,phase:"generating_questions",nextAction:`Still generating. Call mist_plan with { projectPath, conversationId: "${n}" } IMMEDIATELY \u2014 do NOT run bash sleep. The server holds each poll open up to ~10s and returns as soon as questions are ready.`})):u.status==="plan_pending"?c(JSON.stringify({status:"running",conversationId:n,phase:"generating_plan",nextAction:`Plan is being generated (build_plan + image enrichment, 30-60s typical). Call mist_plan with { projectPath, conversationId: "${n}" } IMMEDIATELY \u2014 do NOT run bash sleep. The server holds each poll open up to ~10s and returns as soon as the plan is ready.`})):c(JSON.stringify(u))}catch(u){let g=u instanceof Error?u.message:String(u);return c(`Could not poll plan conversation '${n}': ${g}`,!0)}let E=r??"";if(!E.trim()&&!n&&!S&&!s&&!a&&!l)return c("mist_plan requires one of:\n \u2022 `description` \u2014 first call with a new app idea\n \u2022 `conversationId` alone \u2014 poll an in-flight discovery / plan-gen call\n \u2022 `conversationId` + `answers` \u2014 submit answers after `status: 'clarify'`\n \u2022 `designConversationId` + `designDirection` \u2014 submit a design pick after `status: 'design_clarify_pending'`\n \u2022 `existingPlanId` + a modification `description` \u2014 edit an existing plan\n \u2022 `templateToken` \u2014 fork a published template\n\nThe most common confusion: after `design_clarify_pending`, the next call needs `designConversationId` + `designDirection`, NOT `conversationId` alone.",!0);if(S&&!A&&!E.trim()&&!i)return c(`You passed designConversationId='${S}' but no designDirection. After the user picks one of the directions from the preview, pass it back as: mist_plan({ designConversationId: '${S}', designDirection: { id: '<their-pick-id>' } }). If the user asked for something custom, pass designDirection: { custom: '<their description>' }.`,!0);let T=s;if(!T&&a&&(T=Wa(a)??void 0,!T))return c("Your previous plan is no longer available. Please describe your app again to generate a new plan.",!0);let j=n;if(!X())return c("No Mistflow credentials found. Run mist_setup to connect your account.",!0);let L;if(!j&&!T&&!l){if(!ja(o))return c(`projectPath must be an absolute path \u2014 received '${o}'. Pass the full absolute path to the user's project directory (e.g. /Users/alice/projects/my-app).`,!0);let u=qa(o);if(u!=="mistflow"&&Ba(o))return c(JSON.stringify({status:"unsafe_cwd",projectPath:o,instruction:[`The projectPath you passed (${o}) is a system-level directory (home root, Desktop, Documents, Downloads, or /tmp).`,"Scaffolding here would drop node_modules and a git repo directly at that location, which is messy and hard to clean up.","MANDATORY: Before calling mist_plan again:"," 1. Pick a subfolder name based on the app description (e.g. 'nutrition-tracker', 'habit-app').",` 2. Create the subfolder at ${o}/<subfolder-name>.`," 3. Call mist_plan again with the SAME description and projectPath set to the new subfolder.","Do NOT ask the user where to put the project \u2014 just pick a sensible subfolder name from their description."].join(`
2230
2230
  `)}),!0);if(u==="foreign"&&!x){if(!(b?Fa(b,o,E):!1)){let k=$a(o,E);return c(JSON.stringify({status:"confirm_new_project",projectPath:o,description:E,confirmToken:k,askUserQuestion:{question:"You're inside an existing project directory. Do you want to scaffold a new Mistflow app here, or edit the existing codebase directly?",header:"Scope",options:[{label:"Scaffold a new Mistflow app in a subdirectory",description:"Creates a fresh project in this folder without touching the existing code."},{label:"Edit this existing codebase directly",description:"Cancel Mistflow. Handle the request by editing the current project's files."}],multiSelect:!1},instruction:["The user is inside an existing project (package.json found up the directory tree, no mistflow.json) and did NOT explicitly invoke Mistflow by name.","MANDATORY: Use the AskUserQuestion tool with the provided askUserQuestion to confirm their intent before calling mist_plan again.","If they pick 'Scaffold a new Mistflow app in a subdirectory', call mist_plan again with the SAME description and confirmToken set to the token returned above.","If they pick 'Edit this existing codebase directly', DO NOT call mist_plan again. Fulfill their request by editing files directly in the current project.",b?"The previous confirmToken was invalid, expired, or did not match the current directory/description. Use the fresh token above.":""].filter(Boolean).join(`
2231
- `)}))}L="Note: You're inside an existing project. Mistflow will create the new app in a subdirectory. It won't modify this codebase."}else u==="foreign"&&x&&(L="Note: You're inside an existing project. Mistflow will create the new app in a subdirectory. It won't modify this codebase.")}if(l)try{if(!(await Vo(l)).plan)return c("This template has no plan to fork. Try a different template.",!0);let g=await Jo(l),k=g.plan,P="";if(d&&g.has_source)try{let se=await zt(g.plan,d),B=se.plan??se,jt=se.diff,yo=B?.steps??[],J=new Set([...(jt?.added??[]).map(de=>de.number),...(jt?.modified??[]).map(de=>de.number)]),gt=yo.map(de=>{let Js=de.number;return J.has(Js)?{...de,status:"pending"}:{...de,status:"completed",source:"forked"}});B.steps=gt,k=B;let Ke=gt.filter(de=>de.status==="pending").length;P=` Remixed: ${gt.filter(de=>de.status==="completed").length} steps unchanged, ${Ke} steps need re-implementation.`}catch(se){console.error("[plan] Remix failed, using original plan:",se),P=" (Remix failed \u2014 using original plan. You can modify it later.)"}let Y=Dn(),V=le(Le(),".mistflow","plans");oo(V,{recursive:!0}),ro(le(V,`${Y}.json`),JSON.stringify({plan:k,projectId:g.id,sourceDeploymentId:g.source_deployment_id,forkToken:g.fork_token,requiredEnvVars:g.required_env_vars,dbProvider:g.db_provider}));let te=k?.name??"forked-app",ne=g.has_source,ht=ne?"Source code will be restored during init. Run init promptly \u2014 the download token expires in 1 hour.":"",ze=g.deploy_url?` Instant deploy started \u2014 your app will be live at ${g.deploy_url} in under a minute.`:"";return c(JSON.stringify({planId:Y,forkedFrom:g.forked_from,projectId:g.id,hasSource:ne,deployUrl:g.deploy_url,message:`Forked "${g.forked_from}" into your workspace.${P}${ze} ${ht} NEXT: Call mist_init, name='${te}', and planId='${Y}' to create the project now.`}))}catch(u){let g=u instanceof Error?u.message:"Failed to fork template";return c(g,!0)}if(T){let u;try{u=await zt(T,E)}catch(V){let te=V instanceof Error?V.message:"Failed to modify plan";return c(te,!0)}let g=u.plan,k=u.diff,P=[];if(k?.added?.length){let V=k.added.map(te=>te.title);P.push(`Added ${V.length} step(s): ${V.join(", ")}`)}if(k?.removed?.length){let V=k.removed.map(te=>te.title);P.push(`Removed ${V.length} step(s): ${V.join(", ")}`)}if(k?.modified?.length){let V=k.modified.map(te=>te.title);P.push(`Modified ${V.length} step(s): ${V.join(", ")}`)}let Y=P.length>0?P.join(". "):"No changes detected.";return c(JSON.stringify({plan:g,diff:k,message:`Plan modified. ${Y}. Update mistflow.json with the new plan, then continue with mist_implement.`}))}let v=f?.trim()||void 0,M=i;if(Array.isArray(i)){let u=i.findIndex(g=>g&&typeof g=="object"&&g.decisionKey==="urlChoice");if(u>=0){let g=i[u];!v&&g.answer&&(v=g.answer);let k=i.slice(0,u).concat(i.slice(u+1));M=k.length>0?k:void 0}}else if(i!=null){let u=i;if(!v&&to in u&&(v=u[to]),!v&&"urlChoice"in u&&(v=u.urlChoice),to in u||"urlChoice"in u){let{[to]:g,urlChoice:k,...P}=u;M=Object.keys(P).length>0?P:void 0}}if(v&&(v=v.replace(/^Keep\s+/i,"").replace(/\s*\(Recommended\)\s*$/i,"").replace(/\.mistflow\.app.*$/i,"").trim()||void 0),v){let u=v.toLowerCase().replace(/\s+/g,"-");/^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/.test(u)?v=u:(console.error(`[mist_plan] Discarding urlChoice '${v}' \u2014 does not look like a subdomain. Backend will auto-generate.`),v=void 0)}let H;if(A){H={...A};let u={fontsHint:"fonts_hint",colorMood:"color_mood",heroHeadline:"hero_headline",ctaText:"cta_text",bodySample:"body_sample",heroTreatment:"hero_treatment",shapeLang:"shape_lang",decorationHint:"decoration_hint"};for(let[g,k]of Object.entries(u))A[g]!==void 0&&H[k]===void 0&&(H[k]=A[g])}let W=i?"Generating plan with your answers (LLM call)":S?"Finalizing design direction":"Thinking through discovery questions",oe=e?eo(e.server,e.progressToken,()=>W):{stop:()=>{}};e&&(e.cleanup=()=>oe.stop());let N;try{j&&!M&&!S&&!T&&!a?N=await Bt(j):N=await Eo(E,{conversationId:j,answers:M,autonomous:h,language:m,designConversationId:S,designDirection:H})}catch(u){oe.stop();let g=u instanceof Error?u.message:"Failed to generate plan";return c(g,!0)}if(N.status==="clarify_pending"){oe.stop();let u=N;return c(JSON.stringify({status:"running",conversationId:u.conversation_id,phase:"generating_questions",nextAction:`Discovery questions are generating. Call mist_plan with { projectPath, conversationId: "${u.conversation_id}" } IMMEDIATELY \u2014 do NOT run bash sleep between polls. The server holds each poll open up to ~10s and returns as soon as questions land. Do NOT re-send description or answers.`}))}if(N.status==="plan_pending"){oe.stop();let u=N;return c(JSON.stringify({status:"running",conversationId:u.conversation_id,phase:"generating_plan",nextAction:`Plan is being generated (build_plan + image enrichment, 30-60s typical). Call mist_plan with { projectPath, conversationId: "${u.conversation_id}" } IMMEDIATELY \u2014 do NOT run bash sleep between polls. The server holds each poll open up to ~10s and returns as soon as the plan lands. Do NOT re-send answers.`}))}if(N.status==="design_clarify_pending"&&(W="Generating creative design directions",N=await Ga(N)),oe.stop(),N.status==="clarify"){let u=N.reflection||"",g=N.suggestedName||"",k=N.suggestedFeatures??[],P=N.questions??[],Y=P.some(J=>Array.isArray(J.options)&&typeof J.options[0]=="object"&&J.options[0]?.label),V={primaryActor:"Users",primaryAction:"Core action",surfaceType:"App type",audienceType:"Audience",multiRole:"Roles",publicLanding:"Landing page",realMoney:"Payments",scheduling:"Scheduling",authModel:"Access",dbProvider:"Database",integrations:"Integration",entityShape:"Item shape",coreView:"View",scope:"Scope",sharing:"Sharing",workflow:"Workflow",constraints:"Constraints",domain:"Product"},te=P.map(J=>{let gt=J.decisionKey&&V[J.decisionKey]||Ha(J.question),Ke;return Y&&Array.isArray(J.options)?Ke=J.options.map(Ye=>({label:Ye.label,description:Ye.description??""})):Array.isArray(J.options)?Ke=J.options.map((Ye,de)=>({label:de===0?`${Ye} (Recommended)`:String(Ye),description:J.why??""})):Ke=[{label:"Yes (Recommended)",description:J.why??""},{label:"No",description:""}],{question:J.question,header:gt,options:Ke,multiSelect:!1}}),ht=N.decisions?.audienceType??null,ze=k.length>0?_n(E,{primaryActor:null,primaryAction:null,surfaceType:null,audienceType:ht,multiRole:null,publicLanding:null,realMoney:null,scheduling:null,authModel:null,dbProvider:null,integrations:null},{suggestedName:g,suggestedFeatures:k,language:m}):null,se=ze?Rn(ze):"",B=ar(g||"my-app").slice(0,32);try{let J=await _o(B);!J.available&&J.suggestion&&(B=J.suggestion)}catch{}se&&(se+=`
2231
+ `)}))}L="Note: You're inside an existing project. Mistflow will create the new app in a subdirectory. It won't modify this codebase."}else u==="foreign"&&x&&(L="Note: You're inside an existing project. Mistflow will create the new app in a subdirectory. It won't modify this codebase.")}if(l)try{if(!(await Vo(l)).plan)return c("This template has no plan to fork. Try a different template.",!0);let g=await Jo(l),k=g.plan,I="";if(d&&g.has_source)try{let oe=await zt(g.plan,d),B=oe.plan??oe,jt=oe.diff,yo=B?.steps??[],J=new Set([...(jt?.added??[]).map(de=>de.number),...(jt?.modified??[]).map(de=>de.number)]),gt=yo.map(de=>{let Js=de.number;return J.has(Js)?{...de,status:"pending"}:{...de,status:"completed",source:"forked"}});B.steps=gt,k=B;let Ke=gt.filter(de=>de.status==="pending").length;I=` Remixed: ${gt.filter(de=>de.status==="completed").length} steps unchanged, ${Ke} steps need re-implementation.`}catch(oe){console.error("[plan] Remix failed, using original plan:",oe),I=" (Remix failed \u2014 using original plan. You can modify it later.)"}let Y=Dn(),V=le(Le(),".mistflow","plans");oo(V,{recursive:!0}),ro(le(V,`${Y}.json`),JSON.stringify({plan:k,projectId:g.id,sourceDeploymentId:g.source_deployment_id,forkToken:g.fork_token,requiredEnvVars:g.required_env_vars,dbProvider:g.db_provider}));let te=k?.name??"forked-app",se=g.has_source,ht=se?"Source code will be restored during init. Run init promptly \u2014 the download token expires in 1 hour.":"",ze=g.deploy_url?` Instant deploy started \u2014 your app will be live at ${g.deploy_url} in under a minute.`:"";return c(JSON.stringify({planId:Y,forkedFrom:g.forked_from,projectId:g.id,hasSource:se,deployUrl:g.deploy_url,message:`Forked "${g.forked_from}" into your workspace.${I}${ze} ${ht} NEXT: Call mist_init, name='${te}', and planId='${Y}' to create the project now.`}))}catch(u){let g=u instanceof Error?u.message:"Failed to fork template";return c(g,!0)}if(T){let u;try{u=await zt(T,E)}catch(V){let te=V instanceof Error?V.message:"Failed to modify plan";return c(te,!0)}let g=u.plan,k=u.diff,I=[];if(k?.added?.length){let V=k.added.map(te=>te.title);I.push(`Added ${V.length} step(s): ${V.join(", ")}`)}if(k?.removed?.length){let V=k.removed.map(te=>te.title);I.push(`Removed ${V.length} step(s): ${V.join(", ")}`)}if(k?.modified?.length){let V=k.modified.map(te=>te.title);I.push(`Modified ${V.length} step(s): ${V.join(", ")}`)}let Y=I.length>0?I.join(". "):"No changes detected.";return c(JSON.stringify({plan:g,diff:k,message:`Plan modified. ${Y}. Update mistflow.json with the new plan, then continue with mist_implement.`}))}let v=f?.trim()||void 0,M=i;if(Array.isArray(i)){let u=i.findIndex(g=>g&&typeof g=="object"&&g.decisionKey==="urlChoice");if(u>=0){let g=i[u];!v&&g.answer&&(v=g.answer);let k=i.slice(0,u).concat(i.slice(u+1));M=k.length>0?k:void 0}}else if(i!=null){let u=i;if(!v&&to in u&&(v=u[to]),!v&&"urlChoice"in u&&(v=u.urlChoice),to in u||"urlChoice"in u){let{[to]:g,urlChoice:k,...I}=u;M=Object.keys(I).length>0?I:void 0}}if(v&&(v=v.replace(/^Keep\s+/i,"").replace(/\s*\(Recommended\)\s*$/i,"").replace(/\.mistflow\.app.*$/i,"").trim()||void 0),v){let u=v.toLowerCase().replace(/\s+/g,"-");/^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/.test(u)?v=u:(console.error(`[mist_plan] Discarding urlChoice '${v}' \u2014 does not look like a subdomain. Backend will auto-generate.`),v=void 0)}let H;if(A){H={...A};let u={fontsHint:"fonts_hint",colorMood:"color_mood",heroHeadline:"hero_headline",ctaText:"cta_text",bodySample:"body_sample",heroTreatment:"hero_treatment",shapeLang:"shape_lang",decorationHint:"decoration_hint"};for(let[g,k]of Object.entries(u))A[g]!==void 0&&H[k]===void 0&&(H[k]=A[g])}let W=i?"Generating plan with your answers (LLM call)":S?"Finalizing design direction":"Thinking through discovery questions",re=e?eo(e.server,e.progressToken,()=>W):{stop:()=>{}};e&&(e.cleanup=()=>re.stop());let N;try{j&&!M&&!S&&!T&&!a?N=await Bt(j):N=await Eo(E,{conversationId:j,answers:M,autonomous:h,language:m,designConversationId:S,designDirection:H})}catch(u){re.stop();let g=u instanceof Error?u.message:"Failed to generate plan";return c(g,!0)}if(N.status==="clarify_pending"){re.stop();let u=N;return c(JSON.stringify({status:"running",conversationId:u.conversation_id,phase:"generating_questions",nextAction:`Discovery questions are generating. Call mist_plan with { projectPath, conversationId: "${u.conversation_id}" } IMMEDIATELY \u2014 do NOT run bash sleep between polls. The server holds each poll open up to ~10s and returns as soon as questions land. Do NOT re-send description or answers.`}))}if(N.status==="plan_pending"){re.stop();let u=N;return c(JSON.stringify({status:"running",conversationId:u.conversation_id,phase:"generating_plan",nextAction:`Plan is being generated (build_plan + image enrichment, 30-60s typical). Call mist_plan with { projectPath, conversationId: "${u.conversation_id}" } IMMEDIATELY \u2014 do NOT run bash sleep between polls. The server holds each poll open up to ~10s and returns as soon as the plan lands. Do NOT re-send answers.`}))}if(N.status==="design_clarify_pending"&&(W="Generating creative design directions",N=await Ga(N)),re.stop(),N.status==="clarify"){let u=N.reflection||"",g=N.suggestedName||"",k=N.suggestedFeatures??[],I=N.questions??[],Y=I.some(J=>Array.isArray(J.options)&&typeof J.options[0]=="object"&&J.options[0]?.label),V={primaryActor:"Users",primaryAction:"Core action",surfaceType:"App type",audienceType:"Audience",multiRole:"Roles",publicLanding:"Landing page",realMoney:"Payments",scheduling:"Scheduling",authModel:"Access",dbProvider:"Database",integrations:"Integration",entityShape:"Item shape",coreView:"View",scope:"Scope",sharing:"Sharing",workflow:"Workflow",constraints:"Constraints",domain:"Product"},te=I.map(J=>{let gt=J.decisionKey&&V[J.decisionKey]||Ha(J.question),Ke;return Y&&Array.isArray(J.options)?Ke=J.options.map(Ye=>({label:Ye.label,description:Ye.description??""})):Array.isArray(J.options)?Ke=J.options.map((Ye,de)=>({label:de===0?`${Ye} (Recommended)`:String(Ye),description:J.why??""})):Ke=[{label:"Yes (Recommended)",description:J.why??""},{label:"No",description:""}],{question:J.question,header:gt,options:Ke,multiSelect:!1}}),ht=N.decisions?.audienceType??null,ze=k.length>0?_n(E,{primaryActor:null,primaryAction:null,surfaceType:null,audienceType:ht,multiRole:null,publicLanding:null,realMoney:null,scheduling:null,authModel:null,dbProvider:null,integrations:null},{suggestedName:g,suggestedFeatures:k,language:m}):null,oe=ze?Rn(ze):"",B=ar(g||"my-app").slice(0,32);try{let J=await _o(B);!J.available&&J.suggestion&&(B=J.suggestion)}catch{}oe&&(oe+=`
2232
2232
 
2233
- **Your app URL:** https://${B}.mistflow.app`);let jt={question:`Your app will be at ${B}.mistflow.app \u2014 want to customize the URL?`,decisionKey:"urlChoice",recommended:`Keep ${B}.mistflow.app`,why:"This URL matches your app name and is available. You can customize it now \u2014 subdomains are locked in at scaffold time.",options:[{label:`Keep ${B}.mistflow.app`,description:"This URL is available"},{label:"Choose a different URL",description:"Type your preferred subdomain"}]};P.push(jt);let yo={question:`Your app will be at ${B}.mistflow.app \u2014 want to customize the URL?`,header:"URL",options:[{label:`Keep ${B}.mistflow.app (Recommended)`,description:"This URL is available"},{label:"Choose a different URL",description:"Type your preferred subdomain"}],multiSelect:!1};return te.push(yo),c(JSON.stringify({status:"clarify",conversation_id:N.conversation_id,questions:P,questionCount:P.length,suggestedFeatures:k,suggestedName:g,suggestedSubdomain:B,reflection:u,briefText:se,askUserQuestions:te,planTimingHint:"After the user answers all questions, generating the actual plan takes about 60-90 seconds (backend LLM). Narrate this explicitly before the next mist_plan call.",instruction:[...L?[L,""]:[],u?`${u}
2234
- `:"",se?`Here's what I'd build:
2235
-
2236
- ${se}
2237
- `:"",`I have ${P.length} quick question${P.length===1?"":"s"} to pin down the details.`,"","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550","STOP. DO NOT CONTINUE UNTIL THE USER ACTUALLY ANSWERS THESE QUESTIONS.","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550","","You MUST ask the user via your host's native structured-question","tool (not a text prompt, not a bash echo, not a chat message).","The user has to actually choose. You do NOT get to pick the","'recommended' answer on their behalf \u2014 even if it seems obvious,","even if they said 'just build it', even if you are inside /loop","or any autonomous mode. The recommended label is a hint for the","user, not a permission slip for you to decide. Every question is","a real product decision the user is paying to make.","","Per host (use whichever applies):"," \u2022 Claude Code \u2192 AskUserQuestion tool"," \u2022 Cursor \u2192 quick-pick UI"," \u2022 OpenAI Codex (Plan mode) \u2192 request_user_input tool"," \u2022 OpenAI Codex (Default mode) \u2192 request_user_input returns"," 'unavailable in <mode> mode'. When you see that error, STOP"," your turn, print the questions as a numbered chat message"," with all options visible, and wait for the user's next"," message. Do NOT resume by picking answers yourself."," \u2022 Any host without a native question tool \u2192 same as above:"," stop your turn, print the questions, wait.","","What NOT to do (these have all happened in production transcripts","and are unacceptable):"," \u2717 'I'll go with the recommended defaults \u2014 say the word if you"," want to change any before I continue.' (auto-accepting)"," \u2717 'Locking in the defaults with X as the only override.'"," (inferring answers from the original spec)"," \u2717 Printing the questions + options as markdown and inferring"," answers from silence."," \u2717 Calling mist_plan with answers you picked yourself."," \u2717 Skipping the question UI because /loop is active \u2014 in loop,"," stop the loop and wait for the user. The loop will resume.","","How to call the tool (when available): pass each object in the","`askUserQuestions` array below. Each has `question`, `header`,","`options[]` (each option has `label` + `description`), and","`multiSelect`. The tool returns the user's selected labels.","","Before calling mist_plan again with the answers, tell the user:"," 'Generating your plan now. This takes 30\u201360 seconds \u2014 I'm"," writing the data model, page layout, and build steps.'","Then call mist_plan with:",` conversationId: "${N.conversation_id}"`,` answers: { "<question text>": "<the user's selected label>", ... }`,' urlChoice: "<the URL subdomain the user picked>" \u2190 top-level param, NOT inside answers'," (description is no longer needed \u2014 the server has it from the first call)","","Follow-up clarify rounds are normal \u2014 if the user's answers reveal new","ambiguity, you'll get another `clarify` response. Relay those too,","same way, never inferring. Keep going until the response status is","'ready' or 'design_clarify_pending'.","","IMPORTANT: For the URL question, pass the answer as the top-level 'urlChoice' parameter (not inside answers).",`If the user keeps the default, set urlChoice: "${B}".`,'If they type a custom URL, set urlChoice to just the subdomain part (e.g. "myapp" for "myapp.mistflow.app"). Do not include ".mistflow.app" or any "Keep X" label text \u2014 the tool strips those but passing just the subdomain is cleanest.'].join(`
2238
- `)}))}if(N.status==="design_clarify"){let u=N.directions??[],g=N.plan.name??"your app",k;try{let ne=u.map(B=>({id:B.id,name:B.name,summary:B.summary,hero_headline:B.hero_headline,cta_text:B.cta_text,body_sample:B.body_sample,fonts:B.fonts,colors:B.colors,hero_treatment:B.hero_treatment,shape_lang:B.shape_lang,texture:B.texture,decoration_hint:B.decoration_hint})),ht=En(g,ne),ze=le(o,".mistflow");oo(ze,{recursive:!0});let se=le(ze,"design-directions.html");ro(se,ht,"utf-8"),k=se}catch(ne){console.error(`[mist_plan] design-directions preview render failed: ${ne instanceof Error?ne.message:String(ne)}`)}let P=u.map(ne=>({label:ne.name,description:`${ne.summary} \u2014 ${ne.fonts?.display??""} + ${ne.fonts?.body??""}`}));P.push({label:"Describe your own direction",description:"Skip the proposed options and give me a short description of how the app should feel."});let Y={question:`${g} is planned. Now pick the creative direction \u2014 this shapes fonts, colors, and overall feel.`,header:"Design",options:P,multiSelect:!1},V=k?[`A visual preview of all ${u.length} directions has been written to:`,` ${k}`,"Each card is rendered in its direction's own fonts + palette so the user can see what they're picking \u2014 the picker is meaningless without it.","","OPEN THE PREVIEW NOW. Pick whichever works in your host:",` \u2022 macOS: run open "${k}"`,` \u2022 Linux: run xdg-open "${k}"`,` \u2022 Windows: run start "" "${k}"`," \u2022 No shell access: tell the user the exact path and ask them to open it in their browser before they answer.",'Do NOT skip the preview. "I described the options in text" is NOT a replacement \u2014 the user needs to SEE the directions.'].join(`
2233
+ **Your app URL:** https://${B}.mistflow.app`);let jt={question:`Your app will be at ${B}.mistflow.app \u2014 want to customize the URL?`,decisionKey:"urlChoice",recommended:`Keep ${B}.mistflow.app`,why:"This URL matches your app name and is available. You can customize it now \u2014 subdomains are locked in at scaffold time.",options:[{label:`Keep ${B}.mistflow.app`,description:"This URL is available"},{label:"Choose a different URL",description:"Type your preferred subdomain"}]};I.push(jt);let yo={question:`Your app will be at ${B}.mistflow.app \u2014 want to customize the URL?`,header:"URL",options:[{label:`Keep ${B}.mistflow.app (Recommended)`,description:"This URL is available"},{label:"Choose a different URL",description:"Type your preferred subdomain"}],multiSelect:!1};return te.push(yo),c(JSON.stringify({status:"clarify",nextAction:"STOP \u2014 DO NOT submit answers yourself. Render every entry in `askUserQuestions` via your host's native question tool (AskUserQuestion in Claude Code, request_user_input in OpenAI Codex Plan mode, quick pick in Cursor) and WAIT for the user to actually answer. If your host has no native question tool, stop your turn, print the questions verbatim as a numbered list with all options visible, and wait for the user's next message. Calling mist_plan with answers in the same turn you received the questions is ALWAYS wrong, regardless of how obvious `recommended` looks.",conversation_id:N.conversation_id,questions:I,questionCount:I.length,suggestedFeatures:k,suggestedName:g,suggestedSubdomain:B,reflection:u,briefText:oe,askUserQuestions:te,planTimingHint:"After the user answers all questions, generating the actual plan takes about 60-90 seconds (backend LLM). Narrate this explicitly before the next mist_plan call.",instruction:["\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550","STOP. DO NOT CONTINUE UNTIL THE USER ACTUALLY ANSWERS THESE QUESTIONS.","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550","","You MUST ask the user via your host's native structured-question","tool (not a text prompt, not a bash echo, not a chat message).","The user has to actually choose. You do NOT get to pick the","'recommended' answer on their behalf \u2014 even if it seems obvious,","even if they said 'just build it', even if you are inside /loop","or any autonomous mode. The recommended label is a hint for the","user, not a permission slip for you to decide. Every question is","a real product decision the user is paying to make.","","Per host (use whichever applies):"," \u2022 Claude Code \u2192 AskUserQuestion tool"," \u2022 Cursor \u2192 quick-pick UI"," \u2022 OpenAI Codex (Plan mode) \u2192 request_user_input tool"," \u2022 OpenAI Codex (Default mode) \u2192 request_user_input returns"," 'unavailable in <mode> mode'. When you see that error, STOP"," your turn, print the questions as a numbered chat message"," with all options visible, and wait for the user's next"," message. Do NOT resume by picking answers yourself."," \u2022 Any host without a native question tool \u2192 same as above:"," stop your turn, print the questions, wait.","","What NOT to do (these have all happened in production transcripts","and are unacceptable):"," \u2717 'I'll go with the recommended defaults \u2014 say the word if you"," want to change any before I continue.' (auto-accepting)"," \u2717 'Locking in the defaults with X as the only override.'"," (inferring answers from the original spec)"," \u2717 Printing the questions + options as markdown and inferring"," answers from silence."," \u2717 Calling mist_plan with answers you picked yourself."," \u2717 Skipping the question UI because /loop is active \u2014 in loop,"," stop the loop and wait for the user. The loop will resume.","","How to call the tool (when available): pass each object in the","`askUserQuestions` array below. Each has `question`, `header`,","`options[]` (each option has `label` + `description`), and","`multiSelect`. The tool returns the user's selected labels.","","Before calling mist_plan again with the answers, tell the user:"," 'Generating your plan now. This takes 30\u201360 seconds \u2014 I'm"," writing the data model, page layout, and build steps.'","Then call mist_plan with:",` conversationId: "${N.conversation_id}"`,` answers: { "<question text>": "<the user's selected label>", ... }`,' urlChoice: "<the URL subdomain the user picked>" \u2190 top-level param, NOT inside answers'," (description is no longer needed \u2014 the server has it from the first call)","","Follow-up clarify rounds are normal \u2014 if the user's answers reveal new","ambiguity, you'll get another `clarify` response. Relay those too,","same way, never inferring. Keep going until the response status is","'ready' or 'design_clarify_pending'.","","IMPORTANT: For the URL question, pass the answer as the top-level 'urlChoice' parameter (not inside answers).",`If the user keeps the default, set urlChoice: "${B}".`,'If they type a custom URL, set urlChoice to just the subdomain part (e.g. "myapp" for "myapp.mistflow.app"). Do not include ".mistflow.app" or any "Keep X" label text \u2014 the tool strips those but passing just the subdomain is cleanest.',...u||oe?["","\u2500\u2500\u2500 BACKGROUND (not for you to summarize and proceed \u2014 it's what the user will see when you show them the questions) \u2500\u2500\u2500",...u?[u]:[],...oe?["",oe]:[]]:[],...L?["",L]:[]].join(`
2234
+ `)}))}if(N.status==="design_clarify"){let u=N.directions??[],g=N.plan.name??"your app",k;try{let se=u.map(B=>({id:B.id,name:B.name,summary:B.summary,hero_headline:B.hero_headline,cta_text:B.cta_text,body_sample:B.body_sample,fonts:B.fonts,colors:B.colors,hero_treatment:B.hero_treatment,shape_lang:B.shape_lang,texture:B.texture,decoration_hint:B.decoration_hint})),ht=En(g,se),ze=le(o,".mistflow");oo(ze,{recursive:!0});let oe=le(ze,"design-directions.html");ro(oe,ht,"utf-8"),k=oe}catch(se){console.error(`[mist_plan] design-directions preview render failed: ${se instanceof Error?se.message:String(se)}`)}let I=u.map(se=>({label:se.name,description:`${se.summary} \u2014 ${se.fonts?.display??""} + ${se.fonts?.body??""}`}));I.push({label:"Describe your own direction",description:"Skip the proposed options and give me a short description of how the app should feel."});let Y={question:`${g} is planned. Now pick the creative direction \u2014 this shapes fonts, colors, and overall feel.`,header:"Design",options:I,multiSelect:!1},V=k?[`A visual preview of all ${u.length} directions has been written to:`,` ${k}`,"Each card is rendered in its direction's own fonts + palette so the user can see what they're picking \u2014 the picker is meaningless without it.","","OPEN THE PREVIEW NOW. Pick whichever works in your host:",` \u2022 macOS: run open "${k}"`,` \u2022 Linux: run xdg-open "${k}"`,` \u2022 Windows: run start "" "${k}"`," \u2022 No shell access: tell the user the exact path and ask them to open it in their browser before they answer.",'Do NOT skip the preview. "I described the options in text" is NOT a replacement \u2014 the user needs to SEE the directions.'].join(`
2239
2235
  `):"No visual preview rendered (see server logs). The question options below carry fonts + mood so the user can still pick, but warn them the HTML preview didn't land.",te=k?`open "${k}"`:"";return c(JSON.stringify({status:"design_clarify",designConversationId:N.design_conversation_id,directions:u,previewPath:k,askUserQuestion:Y,instruction:[`The plan for "${g}" is ready. I've proposed ${u.length} creative directions \u2014 each commits to a specific aesthetic (fonts, colors, voice).`,"","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550","STOP. DO NOT PICK A DIRECTION YOURSELF. THE USER PICKS.","\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550","",V,te?`Run this command to open the preview for the user: ${te}`:"","","Then ASK THE USER which direction they want. Use whichever your host supports:"," \u2022 Claude Code \u2192 AskUserQuestion tool with the directionQuestion payload above"," \u2022 Cursor \u2192 quick-pick UI"," \u2022 OpenAI Codex (Plan mode) \u2192 request_user_input tool"," \u2022 OpenAI Codex (Default mode) OR any host without a native question tool:"," STOP your turn. Print the direction names + one-line summaries as a"," numbered chat message and wait for the user's reply. Do NOT resume.","","What NOT to do (these have all happened in production transcripts and are unacceptable):"," \u2717 'I'll go with a custom ops-focused brief that fits better than the default.'"," (auto-picking a direction the user never chose)"," \u2717 'Submitting a custom design brief now so we can keep moving.'"," \u2717 Calling mist_plan with designDirection: { custom: '<your own description>' }"," because the user didn't respond fast enough."," \u2717 Skipping this picker because you already decided on the design yourself."," \u2717 Opening the HTML preview but not actually asking the user anything.","","Once the user picks a direction, call mist_plan with:",` designConversationId: "${N.design_conversation_id}"`," designDirection: <the full direction object from the 'directions' array that the user picked>","(No description needed \u2014 server has it from the first call.)","","IF the user picks 'Describe your own direction':"," Ask them a short open question ('How should the app feel? Any fonts or colors in mind?'),"," wait for a real answer, then call mist_plan with"," designDirection: { custom: '<their exact words>' } + the same designConversationId.","","The next mist_plan call takes ~10-20s \u2014 that's the LLM generating the final DESIGN.md with the picked direction. Tell the user 'Locking in the direction now \u2014 this takes about 15 seconds.' before calling."].filter(Boolean).join(`
2240
- `)}))}let _=N.plan,re=_.name??"Untitled App",ce=N.methodology,D=_.steps;if(!Array.isArray(D)||D.length===0)return c("Plan generation incomplete \u2014 the plan is missing implementation steps. Please call mist_plan again with the same description to retry.",!0);let Q=_.publicPages;if(!Q||Array.isArray(Q)&&Q.length===0){let u=_.pages,g=D.some(P=>typeof P.name=="string"&&P.name.toLowerCase().includes("landing")||typeof P.title=="string"&&P.title.toLowerCase().includes("landing")),k=Array.isArray(u)&&u.some(P=>P.path==="/"||P.route==="/");g||k?Q=["/","/pricing"]:Q=["/"]}let Se=_.primaryAction;if(!Se){let u=_.features;if(Array.isArray(u)&&u.length>0){let k=u.find(Y=>typeof Y.priority=="string"&&Y.priority.toLowerCase()==="must-have")??u[0];Se={entity:k.name??k.title??"item",action:"create",fromPage:"/dashboard"}}}let ue=_.nonNegotiables;(!ue||Array.isArray(ue)&&ue.length===0)&&(ue=["Landing page renders correctly at / with content (not a redirect)","Core user action works end-to-end (create entity, see it in list)"]);let Te=Dn(),$e=le(Le(),".mistflow","plans");oo($e,{recursive:!0});try{let g=Date.now();for(let k of[$e,le(Le(),".mistflow","mockup-state")])if(Ct(k))for(let P of Ra(k))try{let Y=le(k,P),V=Na(Y).mtimeMs;g-V>6048e5&&Ea(Y)}catch{}}catch{}let F;if(p){let u=Vt(p);u?F=u.id:console.error(`Landing design '${p}' not found \u2014 ignoring. Use mist_project action='landing-designs' to browse available landing designs.`)}let R=y||void 0,z=!!F,ge=D.some(u=>{let g=`${u.name??u.title} ${u.description??""}`.toLowerCase();return g.includes("landing")||g.includes("hero")||g.includes("marketing")||g.includes("homepage")}),G=!z&&ge?un(E,{maxResults:2}):[];G.length>0&&(F=G[0].id,console.error(`Auto-assigned landing layout preset (default): ${G[0].title} (${F})`));let ke={name:_.name,summary:_.summary,dataModel:_.dataModel,pages:_.pages,features:_.features,steps:D.map(u=>({...u,name:u.name??u.title})),design:_.design,landingDesign:F,appStyle:R,dbProvider:_.dbProvider??"neon",authModel:_.authModel,audienceType:_.audienceType??"b2c",roles:_.roles,defaultRole:_.defaultRole,publicPages:Q,navStyle:_.navStyle,multiTenant:_.multiTenant,primaryAction:Se,nonNegotiables:ue,requestedSubdomain:v,...m&&m.toLowerCase()!=="english"?{language:m}:{}};ro(le($e,`${Te}.json`),JSON.stringify({plan:ke,methodology:ce}));let Fe=D.map(u=>`${u.number}. ${u.name??u.title}`),qe="",Be=[],me;!z&&G.length>0&&(Be=G.map(g=>({id:g.id,slug:g.slug,title:g.title,description:g.description,url:`${et()}/designs?tab=landing-designs`})),me={question:"What landing page style fits this app?",header:"Landing Design",options:[...G.map((g,k)=>({label:k===0?`${g.title} (Recommended)`:g.title,description:g.description})),{label:"Design from scratch (Creative)",description:"No template \u2014 the AI designs a bespoke hero from your app's concept using 3D, scroll-driven animation, or particle effects. Higher creative ceiling, takes ~10 min longer, occasionally needs a second pass."},{label:"Browse all landing designs",description:`Not sure? See all landing designs at ${et()}/designs?tab=landing-designs and pass the ID back.`}],multiSelect:!1},qe=` REQUIRED: ask the user which landing design they want using the AskUserQuestion tool with the 'landingDesignQuestion' object before calling mist_init. Do NOT assume the recommended option \u2014 the user may want a different style than we inferred. Recommended: ${G.map(g=>g.title).join(" or ")}. If they pick "Design from scratch", pass landingDesign='freeform' to mist_init. If they pick a specific preset title, look up its ID via mist_project action='landing-designs' and pass that. If they pick the recommended option, pass landingDesign='${G[0].id}' explicitly.`);let _e="",Re=[],Ne=void 0,I=(_.audienceType??"b2c")==="b2c",q={question:"Include a lifestyle photo in your landing page hero? Photos add warmth and human context; pure CSS stays cleaner.",header:"Hero",options:[{label:I?"Yes, add a photo (Recommended)":"Yes, add a photo",description:"Lifestyle photography background with your product preview overlaid \u2014 feels warm and human (like HabitFlow, Airbnb)."},{label:I?"No, CSS only":"No, CSS only (Recommended)",description:"Animated gradients + glassmorphism, no photo \u2014 cleaner and more technical (like Stripe, Linear, Vercel)."}],multiSelect:!1},K=" Also ask the user about the 'heroPhotoQuestion' provided. Once they pick, pass heroPhoto=true (photo) or heroPhoto=false (CSS only) to the mist_init call.",fe="",ee=[];for(let u of D){let g=u.name??u.title,k=u.integrationId;if(k){let P=st(k);if(P){let Y=it(P.id);ee.push({step:g,presetId:P.id,presetName:P.name,envVars:Y?.envVars??[]})}}}if(ee.length>0){let u=ee.flatMap(P=>P.envVars),g=[...new Set(u.map(P=>P.key))];fe=` This plan uses integrations (${ee.map(P=>P.presetName).join(", ")}). Detailed blueprints will be auto-injected during each integration step.${g.length>0?` The user will need these API keys: ${g.join(", ")}.`:""}`}return c(JSON.stringify({planId:Te,name:_.name,summary:_.summary,stepCount:D.length,steps:Fe,design:_.design,...F?{landingDesign:F}:{},...R?{appStyle:R}:{},...Re.length>0?{recommendedAppStyles:Re}:{},...Ne?{appStyleQuestion:Ne}:{},...Be.length>0?{recommendedLandingDesigns:Be}:{},...me?{landingDesignQuestion:me}:{},heroPhotoQuestion:q,...ee.length>0?{integrations:ee.map(u=>({step:u.step,preset:u.presetId,name:u.presetName,envVars:u.envVars}))}:{},message:`Plan generated for "${re}" (${D.length} steps).${F?` Landing layout "${F}" set as default.`:""}${R?` App style "${R}" will be applied across all pages.`:""}${fe}${_e}${qe}${K}`,timingContext:`Planning took ~90 seconds. Building will take roughly ${Math.max(15,D.length*3)}\u2013${D.length*5} minutes total across ${D.length} steps (varies by complexity). Mention this to the user before starting the build so they know what to expect.`,mockupPrompt:`Before building, ask the user: "Would you like to preview a mockup of your app before we start building? You can iterate on the design, or skip straight to building." If the user wants a mockup, call mist_mockup({ planId: '${Te}' }). If the user says skip or "just build it", call mist_init({ planId: '${Te}', path: '<absolute path>' }) immediately.`,...L?{warning:L}:{}}))}var Un={name:"mist_plan",description:["ENTRY POINT for creating a NEW web app, website, internal tool, dashboard, landing page, marketplace, content site, or browser game. Mistflow scaffolds a complete Next.js project. It does NOT edit existing codebases.","","WHEN TO CALL THIS \u2014 route here automatically on natural 'build me X' intent. The user does NOT need to say 'mist' or 'mistflow'. Examples that MUST route here:","\u2022 'build me a habit tracker'","\u2022 'make a site for my bakery'","\u2022 'I want an app where users log workouts'","\u2022 'create a dashboard that shows sales'","\u2022 'build a Wordle clone'","\u2022 'build me a CRM using mist' / 'make a todo app with mistflow' (explicit brand invocation)","","PASSING THE DESCRIPTION: Pass the user's words EXACTLY. Do NOT expand, elaborate, add features, rewrite, or strip anything (including 'using mist' / 'with mistflow'). 'build me a habit tracker using mist' becomes description: 'build me a habit tracker using mist'. The description is preserved verbatim.","","BRAND MENTIONED FLAG: If the user's original request literally contained the word 'mist' or 'mistflow' as an explicit invocation (e.g. 'build me a CRM using mist', 'use mistflow to make a todo app'), set brandMentioned: true. If the user did NOT mention the brand by name, omit brandMentioned (do not set it). Only set brandMentioned when the user literally typed the brand name \u2014 never infer it. Do NOT set brandMentioned for the common English noun 'mist' used in other contexts (e.g. 'app about morning mist').","","SAFETY GATE \u2014 the handler walks up the directory tree to detect if you're inside an existing non-Mistflow codebase (package.json found anywhere up the tree, no mistflow.json). When that happens AND brandMentioned is not set, the handler returns status 'confirm_new_project' with a signed confirmToken and an askUserQuestion. On that response:","\u2022 MANDATORY: use the AskUserQuestion tool with the provided askUserQuestion to ask the user.","\u2022 If the user picks 'Scaffold a new Mistflow app in a subdirectory', call mist_plan again with the SAME description and confirmToken set to the token from the response.","\u2022 If the user picks 'Edit this existing codebase directly', DO NOT call mist_plan again. Fulfill their request by editing files directly.","\u2022 The confirmToken is bound to the projectPath and description. If either changes, you'll get a fresh token and must ask again.","\u2022 You do not need to pre-check the directory yourself. The handler handles detection.","","FOLLOW-UP FLOW (after the plan is being generated):","\u2022 status 'clarify' \u2192 use AskUserQuestion with the provided askUserQuestions, then call mist_plan again with conversationId + answers + the same description.","\u2022 You may receive MULTIPLE 'clarify' rounds \u2014 if the user's answers reveal new ambiguity, the planner will ask follow-up questions. Keep relaying questions until you get status 'ready'. This is normal and produces better plans.","\u2022 status 'ready' \u2192 IMMEDIATELY call mist_init with the returned planId. Do not ask permission.","\u2022 NEVER skip the clarifying questions. The discovery process ensures the right thing gets built.","","EXISTING MISTFLOW PROJECTS (mistflow.json present anywhere up the tree): call this for changes that need a new data model, third-party integration, or multi-step structural change (pass existingPlan or existingPlanId). For simpler features (new pages, UI additions), do NOT call mist_plan, but DO ask the user product questions before building (what fields, what layout, what constraints). Get the spec right, build once. For cosmetic changes and bug fixes, skip questions and edit directly. Use mist_project action='get' for context.","","OTHER MODES: Pass templateToken to fork from a mistflow.ai/t/... shared template. Pass appStyle (53 full-app design systems like 'stripe', 'linear') to apply a design system. Browse via mist_project action='app-styles'. Landing layout presets are auto-assigned based on app description."].join(`
2236
+ `)}))}let _=N.plan,ne=_.name??"Untitled App",ce=N.methodology,D=_.steps;if(!Array.isArray(D)||D.length===0)return c("Plan generation incomplete \u2014 the plan is missing implementation steps. Please call mist_plan again with the same description to retry.",!0);let Q=_.publicPages;if(!Q||Array.isArray(Q)&&Q.length===0){let u=_.pages,g=D.some(I=>typeof I.name=="string"&&I.name.toLowerCase().includes("landing")||typeof I.title=="string"&&I.title.toLowerCase().includes("landing")),k=Array.isArray(u)&&u.some(I=>I.path==="/"||I.route==="/");g||k?Q=["/","/pricing"]:Q=["/"]}let Se=_.primaryAction;if(!Se){let u=_.features;if(Array.isArray(u)&&u.length>0){let k=u.find(Y=>typeof Y.priority=="string"&&Y.priority.toLowerCase()==="must-have")??u[0];Se={entity:k.name??k.title??"item",action:"create",fromPage:"/dashboard"}}}let ue=_.nonNegotiables;(!ue||Array.isArray(ue)&&ue.length===0)&&(ue=["Landing page renders correctly at / with content (not a redirect)","Core user action works end-to-end (create entity, see it in list)"]);let Te=Dn(),$e=le(Le(),".mistflow","plans");oo($e,{recursive:!0});try{let g=Date.now();for(let k of[$e,le(Le(),".mistflow","mockup-state")])if(Ct(k))for(let I of Ra(k))try{let Y=le(k,I),V=Na(Y).mtimeMs;g-V>6048e5&&Ea(Y)}catch{}}catch{}let F;if(p){let u=Vt(p);u?F=u.id:console.error(`Landing design '${p}' not found \u2014 ignoring. Use mist_project action='landing-designs' to browse available landing designs.`)}let R=y||void 0,z=!!F,ge=D.some(u=>{let g=`${u.name??u.title} ${u.description??""}`.toLowerCase();return g.includes("landing")||g.includes("hero")||g.includes("marketing")||g.includes("homepage")}),G=!z&&ge?un(E,{maxResults:2}):[];G.length>0&&(F=G[0].id,console.error(`Auto-assigned landing layout preset (default): ${G[0].title} (${F})`));let ke={name:_.name,summary:_.summary,dataModel:_.dataModel,pages:_.pages,features:_.features,steps:D.map(u=>({...u,name:u.name??u.title})),design:_.design,landingDesign:F,appStyle:R,dbProvider:_.dbProvider??"neon",authModel:_.authModel,audienceType:_.audienceType??"b2c",roles:_.roles,defaultRole:_.defaultRole,publicPages:Q,navStyle:_.navStyle,multiTenant:_.multiTenant,primaryAction:Se,nonNegotiables:ue,requestedSubdomain:v,...m&&m.toLowerCase()!=="english"?{language:m}:{}};ro(le($e,`${Te}.json`),JSON.stringify({plan:ke,methodology:ce}));let Fe=D.map(u=>`${u.number}. ${u.name??u.title}`),qe="",Be=[],me;!z&&G.length>0&&(Be=G.map(g=>({id:g.id,slug:g.slug,title:g.title,description:g.description,url:`${et()}/designs?tab=landing-designs`})),me={question:"What landing page style fits this app?",header:"Landing Design",options:[...G.map((g,k)=>({label:k===0?`${g.title} (Recommended)`:g.title,description:g.description})),{label:"Design from scratch (Creative)",description:"No template \u2014 the AI designs a bespoke hero from your app's concept using 3D, scroll-driven animation, or particle effects. Higher creative ceiling, takes ~10 min longer, occasionally needs a second pass."},{label:"Browse all landing designs",description:`Not sure? See all landing designs at ${et()}/designs?tab=landing-designs and pass the ID back.`}],multiSelect:!1},qe=` REQUIRED: ask the user which landing design they want using the AskUserQuestion tool with the 'landingDesignQuestion' object before calling mist_init. Do NOT assume the recommended option \u2014 the user may want a different style than we inferred. Recommended: ${G.map(g=>g.title).join(" or ")}. If they pick "Design from scratch", pass landingDesign='freeform' to mist_init. If they pick a specific preset title, look up its ID via mist_project action='landing-designs' and pass that. If they pick the recommended option, pass landingDesign='${G[0].id}' explicitly.`);let _e="",Re=[],Ne=void 0,P=(_.audienceType??"b2c")==="b2c",q={question:"Include a lifestyle photo in your landing page hero? Photos add warmth and human context; pure CSS stays cleaner.",header:"Hero",options:[{label:P?"Yes, add a photo (Recommended)":"Yes, add a photo",description:"Lifestyle photography background with your product preview overlaid \u2014 feels warm and human (like HabitFlow, Airbnb)."},{label:P?"No, CSS only":"No, CSS only (Recommended)",description:"Animated gradients + glassmorphism, no photo \u2014 cleaner and more technical (like Stripe, Linear, Vercel)."}],multiSelect:!1},K=" Also ask the user about the 'heroPhotoQuestion' provided. Once they pick, pass heroPhoto=true (photo) or heroPhoto=false (CSS only) to the mist_init call.",fe="",ee=[];for(let u of D){let g=u.name??u.title,k=u.integrationId;if(k){let I=st(k);if(I){let Y=it(I.id);ee.push({step:g,presetId:I.id,presetName:I.name,envVars:Y?.envVars??[]})}}}if(ee.length>0){let u=ee.flatMap(I=>I.envVars),g=[...new Set(u.map(I=>I.key))];fe=` This plan uses integrations (${ee.map(I=>I.presetName).join(", ")}). Detailed blueprints will be auto-injected during each integration step.${g.length>0?` The user will need these API keys: ${g.join(", ")}.`:""}`}return c(JSON.stringify({planId:Te,name:_.name,summary:_.summary,stepCount:D.length,steps:Fe,design:_.design,...F?{landingDesign:F}:{},...R?{appStyle:R}:{},...Re.length>0?{recommendedAppStyles:Re}:{},...Ne?{appStyleQuestion:Ne}:{},...Be.length>0?{recommendedLandingDesigns:Be}:{},...me?{landingDesignQuestion:me}:{},heroPhotoQuestion:q,...ee.length>0?{integrations:ee.map(u=>({step:u.step,preset:u.presetId,name:u.presetName,envVars:u.envVars}))}:{},message:`Plan generated for "${ne}" (${D.length} steps).${F?` Landing layout "${F}" set as default.`:""}${R?` App style "${R}" will be applied across all pages.`:""}${fe}${_e}${qe}${K}`,timingContext:`Planning took ~90 seconds. Building will take roughly ${Math.max(15,D.length*3)}\u2013${D.length*5} minutes total across ${D.length} steps (varies by complexity). Mention this to the user before starting the build so they know what to expect.`,mockupPrompt:`Before building, ask the user: "Would you like to preview a mockup of your app before we start building? You can iterate on the design, or skip straight to building." If the user wants a mockup, call mist_mockup({ planId: '${Te}' }). If the user says skip or "just build it", call mist_init({ planId: '${Te}', path: '<absolute path>' }) immediately.`,...L?{warning:L}:{}}))}var Un={name:"mist_plan",description:["ENTRY POINT for creating a NEW web app, website, internal tool, dashboard, landing page, marketplace, content site, or browser game. Mistflow scaffolds a complete Next.js project. It does NOT edit existing codebases.","","WHEN TO CALL THIS \u2014 route here automatically on natural 'build me X' intent. The user does NOT need to say 'mist' or 'mistflow'. Examples that MUST route here:","\u2022 'build me a habit tracker'","\u2022 'make a site for my bakery'","\u2022 'I want an app where users log workouts'","\u2022 'create a dashboard that shows sales'","\u2022 'build a Wordle clone'","\u2022 'build me a CRM using mist' / 'make a todo app with mistflow' (explicit brand invocation)","","PASSING THE DESCRIPTION: Pass the user's words EXACTLY. Do NOT expand, elaborate, add features, rewrite, or strip anything (including 'using mist' / 'with mistflow'). 'build me a habit tracker using mist' becomes description: 'build me a habit tracker using mist'. The description is preserved verbatim.","","BRAND MENTIONED FLAG: If the user's original request literally contained the word 'mist' or 'mistflow' as an explicit invocation (e.g. 'build me a CRM using mist', 'use mistflow to make a todo app'), set brandMentioned: true. If the user did NOT mention the brand by name, omit brandMentioned (do not set it). Only set brandMentioned when the user literally typed the brand name \u2014 never infer it. Do NOT set brandMentioned for the common English noun 'mist' used in other contexts (e.g. 'app about morning mist').","","SAFETY GATE \u2014 the handler walks up the directory tree to detect if you're inside an existing non-Mistflow codebase (package.json found anywhere up the tree, no mistflow.json). When that happens AND brandMentioned is not set, the handler returns status 'confirm_new_project' with a signed confirmToken and an askUserQuestion. On that response:","\u2022 MANDATORY: use the AskUserQuestion tool with the provided askUserQuestion to ask the user.","\u2022 If the user picks 'Scaffold a new Mistflow app in a subdirectory', call mist_plan again with the SAME description and confirmToken set to the token from the response.","\u2022 If the user picks 'Edit this existing codebase directly', DO NOT call mist_plan again. Fulfill their request by editing files directly.","\u2022 The confirmToken is bound to the projectPath and description. If either changes, you'll get a fresh token and must ask again.","\u2022 You do not need to pre-check the directory yourself. The handler handles detection.","","FOLLOW-UP FLOW (after the plan is being generated):","\u2022 status 'clarify' \u2192 use AskUserQuestion with the provided askUserQuestions, then call mist_plan again with conversationId + answers + the same description.","\u2022 You may receive MULTIPLE 'clarify' rounds \u2014 if the user's answers reveal new ambiguity, the planner will ask follow-up questions. Keep relaying questions until you get status 'ready'. This is normal and produces better plans.","\u2022 status 'ready' \u2192 IMMEDIATELY call mist_init with the returned planId. Do not ask permission.","\u2022 NEVER skip the clarifying questions. The discovery process ensures the right thing gets built.","","EXISTING MISTFLOW PROJECTS (mistflow.json present anywhere up the tree): call this for changes that need a new data model, third-party integration, or multi-step structural change (pass existingPlan or existingPlanId). For simpler features (new pages, UI additions), do NOT call mist_plan, but DO ask the user product questions before building (what fields, what layout, what constraints). Get the spec right, build once. For cosmetic changes and bug fixes, skip questions and edit directly. Use mist_project action='get' for context.","","OTHER MODES: Pass templateToken to fork from a mistflow.ai/t/... shared template. Pass appStyle (53 full-app design systems like 'stripe', 'linear') to apply a design system. Browse via mist_project action='app-styles'. Landing layout presets are auto-assigned based on app description."].join(`
2241
2237
  `),inputSchema:za,handler:Va};import{z as At}from"zod";import{existsSync as Ve,mkdirSync as hr,writeFileSync as gr,readFileSync as so,readdirSync as Hn,copyFileSync as Ya}from"fs";import{join as ve,resolve as Wn,dirname as _t,isAbsolute as Qa}from"path";import{homedir as Xa}from"os";import{spawn as ru}from"child_process";import{randomBytes as Za}from"crypto";import{simpleGit as tl}from"simple-git";pe();We();function $n(t){if(!t)return"Item";let e=t.replace(/([a-z0-9])([A-Z])/g,"$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);return e.length===0?"Item":e.map(r=>r.charAt(0).toUpperCase()+r.slice(1).toLowerCase()).join("")}function Ja(t){return t&&t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").replace(/[^A-Za-z0-9]+/g,"-").toLowerCase().replace(/^-+|-+$/g,"")||"item"}function Ka(t){let e=$n(t);return e.charAt(0).toLowerCase()+e.slice(1)}function dr(){return["# Integration contracts","","This directory holds the single source of truth for every API shape in","this app. Frontend code and backend routes both import from here, so","drift becomes a compile error instead of a silent runtime bug.","","## The convention","","Every entity defined in `db/schema.ts` MUST have a matching contract","file in this directory. Each contract file exports three things:","","- `<Entity>Schema` \u2014 the Zod schema for reading the entity (derived from"," the Drizzle table via `createSelectSchema`).","- `Create<Entity>Input` \u2014 the Zod schema for creating a new row"," (derived via `createInsertSchema`, typically with `id` and"," `createdAt` omitted).","- `<Entity>` \u2014 the inferred TypeScript type from `<Entity>Schema`.","","File name is the kebab-case singular of the entity, e.g. `habit.ts`,","`blog-post.ts`. One entity per file.","","## Example","","```ts","// contracts/habit.ts",'import { createSelectSchema, createInsertSchema } from "drizzle-zod";','import { z } from "zod";','import { habit } from "@/db/schema";',"","export const HabitSchema = createSelectSchema(habit);","export type Habit = z.infer<typeof HabitSchema>;","","export const CreateHabitInput = createInsertSchema(habit).omit({"," id: true,"," createdAt: true,","});","export type CreateHabitInput = z.infer<typeof CreateHabitInput>;","```","","## Usage","","Frontend fetch and backend route should BOTH import from `contracts/`:","","```ts","// app/api/habits/route.ts",'import { CreateHabitInput, HabitSchema } from "@/contracts/habit";',"","export async function POST(request: Request) {"," const body = CreateHabitInput.parse(await request.json());"," const row = await db.insert(habit).values(body).returning();"," return Response.json(HabitSchema.parse(row[0]));","}","```","","```ts","// app/habits/page.tsx",'import type { Habit } from "@/contracts/habit";',"",'const res = await fetch("/api/habits");',"const habits: Habit[] = await res.json();","```","","## Adding a new entity","","1. Add the Drizzle table to `db/schema.ts`.","2. Create the matching `contracts/<entity>.ts` file using the template above.","3. Import from `contracts/` in every route, server action, and client"," component that touches the entity. Never inline the type.","","Run `mist_doctor` to check for entities that are missing a contract.",""].join(`
2242
2238
  `)}function Fn(){return["<!-- mist:contracts:start -->","## Integration contracts","","Every API shape in this app lives in `contracts/`. The directory holds","Zod schemas derived from `db/schema.ts` via `drizzle-zod`. Frontend and","backend both import from `contracts/`, so type drift becomes a compile","error instead of a runtime bug.","","### Rules for AI agents","","1. Every API route (`app/api/**/route.ts`) and server action MUST"," import its request + response types from `contracts/<entity>.ts`."," Never inline a Zod schema or a TypeScript type for an entity that"," already has a contract. Never `z.object({ ... })` inside a route"," handler for a known entity.","2. When you add a new entity to `db/schema.ts`, you MUST create the"," matching contract file BEFORE writing any route that uses it. Order"," matters: schema -> contract -> route.","3. Validate every request body with the contract's `.parse()` method."," If validation fails, return a 400 with the Zod error message \u2014 the"," contract is the boundary, not a decoration.","4. Validate every response body before returning it. Use the select"," schema's `.parse()` on the row(s) you read from the DB. This catches"," the case where the DB shape has drifted from the contract.","5. Client components that fetch an entity MUST import the inferred",' TypeScript type (`import type { Habit } from "@/contracts/habit"`)'," \u2014 never hand-write a duplicate type.","","### Contract file shape","","```ts","// contracts/<entity>.ts",'import { createSelectSchema, createInsertSchema } from "drizzle-zod";','import { z } from "zod";','import { entityTable } from "@/db/schema";',"","export const EntitySchema = createSelectSchema(entityTable);","export type Entity = z.infer<typeof EntitySchema>;","","export const CreateEntityInput = createInsertSchema(entityTable).omit({"," id: true,"," createdAt: true,","});","export type CreateEntityInput = z.infer<typeof CreateEntityInput>;","```","","### Example route using a contract","","```ts","// app/api/entities/route.ts",'import { db } from "@/lib/db";','import { entityTable } from "@/db/schema";','import { CreateEntityInput, EntitySchema } from "@/contracts/entity";',"","export async function POST(request: Request) {"," const body = CreateEntityInput.parse(await request.json());"," const [row] = await db.insert(entityTable).values(body).returning();"," return Response.json(EntitySchema.parse(row));","}","```","","Run `mist_doctor` to check every entity in `db/schema.ts` has a","contract file. Missing contracts are reported as warnings.","<!-- mist:contracts:end -->",""].join(`
2243
2239
  `)}var cr="<!-- mist:contracts:start -->",qn="<!-- mist:contracts:end -->";function pr(t){let e=Fn();if(t.includes(cr)){let o=t.indexOf(cr),n=qn,i=t.indexOf(n,o);if(i===-1)return t.slice(0,o)+e;let s=t.slice(i+n.length);return t.slice(0,o)+e+s.replace(/^\n+/,"")}return t.replace(/\s+$/,"")+`
@@ -2418,24 +2414,24 @@ export default function RootLayout({ children }: { children: React.ReactNode })
2418
2414
  `)}function xl(t){if(!t.multiTenant)return null;let e=[];return e.push('import { db } from "./db";'),e.push('import { organization, orgMember } from "@/db/schema/organization";'),e.push('import { eq } from "drizzle-orm";'),e.push(""),e.push("export async function getCurrentOrg(userId: string) {"),e.push(" const membership = await db"),e.push(" .select()"),e.push(" .from(orgMember)"),e.push(" .where(eq(orgMember.userId, userId))"),e.push(" .limit(1);"),e.push(" if (membership.length === 0) return null;"),e.push(" const org = await db"),e.push(" .select()"),e.push(" .from(organization)"),e.push(" .where(eq(organization.id, membership[0].orgId))"),e.push(" .limit(1);"),e.push(" return org[0] ?? null;"),e.push("}"),e.push(""),e.push("export async function getOrgMembers(orgId: string) {"),e.push(" return db"),e.push(" .select()"),e.push(" .from(orgMember)"),e.push(" .where(eq(orgMember.orgId, orgId));"),e.push("}"),e.push(""),e.push("export async function inviteToOrg(orgId: string, email: string, role: string) {"),e.push(" const id = crypto.randomUUID();"),e.push(" await db.insert(orgMember).values({"),e.push(" id,"),e.push(" orgId,"),e.push(" userId: email,"),e.push(" role,"),e.push(" });"),e.push(" return { id, orgId, email, role };"),e.push("}"),e.push(""),e.join(`
2419
2415
  `)}function kl(t){if(!t.multiTenant)return null;let e=[];return e.push('"use client";'),e.push(""),e.push("import {"),e.push(" DropdownMenu,"),e.push(" DropdownMenuContent,"),e.push(" DropdownMenuItem,"),e.push(" DropdownMenuTrigger,"),e.push('} from "@/components/ui/dropdown-menu";'),e.push('import { Button } from "@/components/ui/button";'),e.push('import { ChevronsUpDown } from "lucide-react";'),e.push(""),e.push("interface OrgSwitcherProps {"),e.push(" orgs: Array<{ id: string; name: string }>;"),e.push(" currentOrgId: string;"),e.push("}"),e.push(""),e.push("export default function OrgSwitcher({ orgs, currentOrgId }: OrgSwitcherProps) {"),e.push(" const currentOrg = orgs.find((o) => o.id === currentOrgId);"),e.push(""),e.push(" return ("),e.push(" <DropdownMenu>"),e.push(" <DropdownMenuTrigger asChild>"),e.push(' <Button variant="outline" className="w-full justify-between">'),e.push(' <span className="truncate">{currentOrg?.name ?? "Select org"}</span>'),e.push(' <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />'),e.push(" </Button>"),e.push(" </DropdownMenuTrigger>"),e.push(' <DropdownMenuContent className="w-56">'),e.push(" {orgs.map((org) => ("),e.push(" <DropdownMenuItem key={org.id}>"),e.push(" {org.name}"),e.push(" </DropdownMenuItem>"),e.push(" ))}"),e.push(" </DropdownMenuContent>"),e.push(" </DropdownMenu>"),e.push(" );"),e.push("}"),e.push(""),e.join(`
2420
2416
  `)}function Sl(t,e,r){let o=[],n=t.split("-").map(l=>l.charAt(0).toUpperCase()+l.slice(1)).join(" ");o.push(`# ${n}`),o.push(""),e?.summary&&(o.push(e.summary),o.push(""));let i=e?.features??[];if(i.length>0){o.push("## Features"),o.push("");for(let l of i){let d=l.description?` \u2014 ${l.description}`:"";o.push(`- **${l.name}**${d}`)}o.push("")}o.push("## Tech Stack"),o.push(""),o.push("| Layer | Technology |"),o.push("|-------|------------|"),o.push("| Framework | Next.js 15 (App Router) |"),o.push("| Database | Mistflow Cloud (Postgres) + Drizzle ORM |"),o.push("| Auth | Better Auth (email/password, social login) |"),o.push("| Styling | Tailwind CSS + shadcn/ui |"),o.push("| Deployment | Mistflow Cloud |"),r.hasStripe&&o.push("| Payments | Stripe |"),r.hasResend&&o.push("| Email | Resend + React Email |"),r.hasStorage&&o.push("| File Storage | Mistflow Cloud (managed blob storage) |"),r.hasAdmin&&o.push("| Admin | Better Auth admin plugin |"),r.hasAI&&o.push("| AI | Vercel AI SDK + OpenAI |"),o.push("");let s=e?.pages??[];if(s.length>0){o.push("## Pages"),o.push(""),o.push("| Route | Description |"),o.push("|-------|-------------|");for(let l of s){let d=l.path??l.route??l.name??"",h=l.description??"";o.push(`| \`${d.startsWith("/")?d:"/"+d}\` | ${h} |`)}o.push("")}let a=e?.dataModel??[];if(a.length>0){o.push("## Data Model"),o.push("");for(let l of a){let d=l.entity??l.name??"Unknown";if(o.push(`### ${d}`),o.push(""),l.fields.length>0){if(typeof l.fields[0]=="string")o.push(`Fields: ${l.fields.join(", ")}`);else{o.push("| Field | Type |"),o.push("|-------|------|");for(let h of l.fields)o.push(`| ${h.name} | ${h.type} |`)}o.push("")}}}return o.push("## Getting Started"),o.push(""),o.push("### Prerequisites"),o.push(""),o.push("- Node.js 20+"),o.push("- npm"),o.push(""),o.push("### Install"),o.push(""),o.push("The host AI calls mist_install (fire-and-poll) which handles this for you. Run manually with plain npm if needed:"),o.push(""),o.push("```bash"),o.push("npm install"),o.push("```"),o.push(""),o.push("### Set up environment"),o.push(""),o.push("Copy `.env.example` to `.env.local` and fill in the values:"),o.push(""),o.push("```bash"),o.push("cp .env.example .env.local"),o.push("```"),o.push(""),o.push("| Variable | Description | Required |"),o.push("|----------|-------------|----------|"),r.isNeon?o.push("| `DATABASE_URL` | Postgres connection URL | Yes |"):(o.push("| `TURSO_URL` | Database connection URL | Yes |"),o.push("| `TURSO_AUTH_TOKEN` | Database auth token | Yes |")),o.push("| `AUTH_SECRET` | Auth encryption secret (auto-generated) | Yes |"),r.hasStripe&&(o.push("| `STRIPE_SECRET_KEY` | Stripe secret key | Yes |"),o.push("| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret | Yes |"),o.push("| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | Stripe publishable key | Yes |")),r.hasResend&&(o.push("| `RESEND_API_KEY` | Resend API key | Yes |"),o.push("| `EMAIL_FROM` | Sender email address | Yes (production) |")),r.hasStorage&&(o.push("| `MISTFLOW_API_KEY` | Mistflow API key for file storage | Yes |"),o.push("| `MISTFLOW_PROJECT_ID` | Mistflow project ID | Yes |")),r.hasAI&&o.push("| `OPENAI_API_KEY` | OpenAI API key | Yes |"),o.push(""),o.push("### Local database"),o.push(""),r.isNeon?(o.push("For local development, start a local Postgres server:"),o.push(""),o.push("```bash"),o.push("# Using Docker:"),o.push("docker run -d --name postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:17"),o.push("# Or install via Homebrew: brew install postgresql@17 && brew services start postgresql@17"),o.push("```")):(o.push("For local development, start a local Turso server:"),o.push(""),o.push("```bash"),o.push("npx turso dev"),o.push("```")),o.push(""),o.push("Then set up the database:"),o.push(""),o.push("```bash"),o.push("npm run db:push"),o.push("```"),o.push(""),o.push("### Run"),o.push(""),o.push("```bash"),o.push("npm run dev"),o.push("```"),o.push(""),o.push("Open [http://localhost:3000](http://localhost:3000)."),o.push(""),o.push("## Project Structure"),o.push(""),o.push("```"),o.push("app/"),o.push(" (auth)/ Login and registration pages"),o.push(" (dashboard)/ Authenticated app pages"),r.hasAdmin&&o.push(" (admin)/ Admin panel pages"),o.push(" api/ API routes (auth, health, webhooks)"),o.push(" layout.tsx Root layout with fonts and providers"),o.push(" globals.css Design tokens and Tailwind config"),o.push("components/ Reusable UI components"),o.push("db/"),o.push(" schema/ Database table definitions"),o.push(" index.ts Schema exports"),o.push("lib/"),o.push(" auth.ts Better Auth server config"),o.push(" auth-client.ts Better Auth client config"),o.push(` db.ts ${r.isNeon?"Postgres":"SQLite"} database connection`),r.hasStripe&&o.push(" stripe.ts Stripe client"),r.hasResend&&(o.push(" resend.ts Resend client"),o.push(" email.ts Email send helpers")),r.hasStorage&&o.push(" storage.ts File upload/download helpers"),r.hasAI&&o.push(" ai.ts AI client (Vercel AI SDK + OpenAI)"),r.hasResend&&o.push("emails/ React Email templates"),o.push("```"),o.push(""),o.push("## Deploy"),o.push(""),o.push("Deploy to production with Mistflow:"),o.push(""),o.push("```"),o.push("# In your AI editor (Claude Code, Cursor, etc.):"),o.push("mist_deploy action='deploy'"),o.push("```"),o.push(""),o.push("Your app will be live at `https://<app-name>.mistflow.app`."),o.push(""),e?.design&&(o.push("## Design"),o.push(""),e.design.tone&&o.push(`- **Tone**: ${e.design.tone}`),e.design.fonts&&(o.push(`- **Heading font**: ${e.design.fonts.heading}`),o.push(`- **Body font**: ${e.design.fonts.body}`)),e.design.accentColor&&o.push(`- **Accent color**: ${e.design.accentColor}`),e.design.borderRadius&&o.push(`- **Border radius**: ${e.design.borderRadius}`),o.push("")),o.push("---"),o.push(""),o.push("Built with [Mistflow](https://mistflow.ai)"),o.push(""),o.join(`
2421
- `)}async function Tl(t,e){let{name:r,plan:o,path:n,planId:i}=t;if(!n)return c("mist_init requires an explicit 'path' \u2014 the absolute directory where the project should be scaffolded. Pass the user's project directory (e.g. /Users/alice/projects/my-app). Do not rely on a default.",!0);if(!Qa(n))return c(`mist_init 'path' must be an absolute path \u2014 received '${n}'. Pass the full absolute path to the target directory.`,!0);let s=Wn(n),a=o;if(!a&&i){let w=el(i);if(!w)return c(`No plan found for planId '${i}'. Call mist_plan first, or pass the plan object inline.`,!0);a=w.plan}let l=a?.design,d=a?.appStyle,h=a?no(a,"stripe","payment","billing","subscription","checkout","pricing"):!1,m=!0,p=a?no(a,"upload","file storage","image upload","profile picture","attachment","gallery","media","blob"):!1,y=a?no(a,"admin panel","admin dashboard","admin management"):!1,x=a?no(a,"ai integration","openai","llm","ai chat","chatbot","gpt"):!1,b=a,f=!0;if(!nl(s))return c(`A project already exists at this location (${s}). Choose a different name, or delete the existing folder first. (A .mistflow/ folder left over from planning is fine \u2014 init will preserve it.)`,!0);hr(s,{recursive:!0});try{let w=ve(_t(s),".mistflow","mockups");if(Ve(w)){let I=Hn(w).filter(q=>q.endsWith(".html"));if(I.length>0){let q=ve(s,".mistflow","mockups");hr(q,{recursive:!0});for(let K of I)Ya(ve(w,K),ve(q,K));console.error(`Copied ${I.length} mockup file(s) into project`)}}}catch(w){console.error("Could not copy mockup files:",w instanceof Error?w.message:w)}let S=null;try{S=await Ho("nextjs")}catch(w){console.error("Could not fetch scaffold from API, using minimal scaffold:",w instanceof Error?w.message:w)}if(S){let w=r.toLowerCase().replace(/[^a-z0-9-]/g,"-");for(let u of S.files){if(u.path==="package.json"||u.path==="middleware.ts"||u.path==="components/sidebar.tsx"||u.path==="components/topnav.tsx"||u.path==="app/(dashboard)/layout.tsx"||u.path==="app/(dashboard)/page.tsx"||u.path==="app/(dashboard)/dashboard/page.tsx"||!h&&(u.path.includes("stripe")||u.path.includes("webhook/stripe"))||!m&&(u.path.includes("resend")||u.path.includes("emails/"))||!y&&(u.path.includes("(admin)")||u.path.includes("admin-sidebar"))||f&&(u.path==="lib/db.ts"||u.path==="lib/auth.ts"||u.path==="drizzle.config.ts"||u.path==="db/schema/auth.ts"))continue;let g=u.content.replace(/\{\{APP_NAME\}\}/g,r).replace(/\{\{WORKER_NAME\}\}/g,w);if(f&&u.path==="next.config.ts"&&(g=g.replace(/serverExternalPackages:\s*\[[^\]]*\],?/g,'serverExternalPackages: ["@electric-sql/pglite"],')),u.path==="next.config.ts"){let k=ol(s);k&&(console.error(`[init] Project is inside monorepo at ${k} \u2014 adding outputFileTracingRoot`),g.includes("outputFileTracingRoot")||(g=g.replace('import type { NextConfig } from "next";',`import type { NextConfig } from "next";
2417
+ `)}async function Tl(t,e){let{name:r,plan:o,path:n,planId:i}=t;if(!n)return c("mist_init requires an explicit 'path' \u2014 the absolute directory where the project should be scaffolded. Pass the user's project directory (e.g. /Users/alice/projects/my-app). Do not rely on a default.",!0);if(!Qa(n))return c(`mist_init 'path' must be an absolute path \u2014 received '${n}'. Pass the full absolute path to the target directory.`,!0);let s=Wn(n),a=o;if(!a&&i){let w=el(i);if(!w)return c(`No plan found for planId '${i}'. Call mist_plan first, or pass the plan object inline.`,!0);a=w.plan}let l=a?.design,d=a?.appStyle,h=a?no(a,"stripe","payment","billing","subscription","checkout","pricing"):!1,m=!0,p=a?no(a,"upload","file storage","image upload","profile picture","attachment","gallery","media","blob"):!1,y=a?no(a,"admin panel","admin dashboard","admin management"):!1,x=a?no(a,"ai integration","openai","llm","ai chat","chatbot","gpt"):!1,b=a,f=!0;if(!nl(s))return c(`A project already exists at this location (${s}). Choose a different name, or delete the existing folder first. (A .mistflow/ folder left over from planning is fine \u2014 init will preserve it.)`,!0);hr(s,{recursive:!0});try{let w=ve(_t(s),".mistflow","mockups");if(Ve(w)){let P=Hn(w).filter(q=>q.endsWith(".html"));if(P.length>0){let q=ve(s,".mistflow","mockups");hr(q,{recursive:!0});for(let K of P)Ya(ve(w,K),ve(q,K));console.error(`Copied ${P.length} mockup file(s) into project`)}}}catch(w){console.error("Could not copy mockup files:",w instanceof Error?w.message:w)}let S=null;try{S=await Ho("nextjs")}catch(w){console.error("Could not fetch scaffold from API, using minimal scaffold:",w instanceof Error?w.message:w)}if(S){let w=r.toLowerCase().replace(/[^a-z0-9-]/g,"-");for(let u of S.files){if(u.path==="package.json"||u.path==="middleware.ts"||u.path==="components/sidebar.tsx"||u.path==="components/topnav.tsx"||u.path==="app/(dashboard)/layout.tsx"||u.path==="app/(dashboard)/page.tsx"||u.path==="app/(dashboard)/dashboard/page.tsx"||!h&&(u.path.includes("stripe")||u.path.includes("webhook/stripe"))||!m&&(u.path.includes("resend")||u.path.includes("emails/"))||!y&&(u.path.includes("(admin)")||u.path.includes("admin-sidebar"))||f&&(u.path==="lib/db.ts"||u.path==="lib/auth.ts"||u.path==="drizzle.config.ts"||u.path==="db/schema/auth.ts"))continue;let g=u.content.replace(/\{\{APP_NAME\}\}/g,r).replace(/\{\{WORKER_NAME\}\}/g,w);if(f&&u.path==="next.config.ts"&&(g=g.replace(/serverExternalPackages:\s*\[[^\]]*\],?/g,'serverExternalPackages: ["@electric-sql/pglite"],')),u.path==="next.config.ts"){let k=ol(s);k&&(console.error(`[init] Project is inside monorepo at ${k} \u2014 adding outputFileTracingRoot`),g.includes("outputFileTracingRoot")||(g=g.replace('import type { NextConfig } from "next";',`import type { NextConfig } from "next";
2422
2418
  import { dirname } from "path";
2423
2419
  import { fileURLToPath } from "url";
2424
2420
 
2425
2421
  const __dirname = dirname(fileURLToPath(import.meta.url));`),g=g.replace("images: {",`outputFileTracingRoot: __dirname,
2426
- images: {`)))}!y&&u.path.includes("sidebar")&&(g=g.replace(/\{user\.role === "admin"[\s\S]*?<\/Link>\s*\)\}/m,""),g=g.replace(/, Shield/g,"")),O(s,u.path,g)}let I={...S.dependencies},q={...S.devDependencies};if(I["drizzle-zod"]||(I["drizzle-zod"]="^0.5.1"),f&&(delete I["@libsql/client"],I["@neondatabase/serverless"]="^0.10.0",q["@electric-sql/pglite"]="^0.2.0"),h&&(I.stripe="^17.0.0"),m&&(I.resend="^4.0.0",I["@react-email/components"]="^0.0.31"),x&&(I.ai="^4.0.0",I["@ai-sdk/openai"]="^1.0.0",I.openai="^4.0.0"),O(s,"package.json",JSON.stringify({name:r,version:"0.1.0",private:!0,scripts:{dev:"next dev",build:"next build","build:cf":"opennextjs-cloudflare build",start:"next start",lint:"next lint","db:push":"drizzle-kit push","db:studio":"drizzle-kit studio"},dependencies:I,devDependencies:q,optionalDependencies:{"@noble/ciphers":"^1.3.0"},overrides:{react:"19.1.0","react-dom":"19.1.0",punycode:"^2.3.1","zod-to-json-schema":"3.24.6"}},null,2)),S.methodology){let u=S.methodology;f&&(u=u.replace(/sqliteTable/g,"pgTable").replace(/drizzle-orm\/sqlite-core/g,"drizzle-orm/pg-core").replace(/Use `text` for dates \(SQLite stores dates as text\)/g,"Use `timestamp` for dates and `boolean` for booleans (native Postgres types)").replace(/text\("created_at"\)\.notNull\(\)\.default\(sql`\(CURRENT_TIMESTAMP\)`\)/g,'timestamp("created_at").notNull().defaultNow()').replace(/text\("updated_at"\)\.notNull\(\)\.default\(sql`\(CURRENT_TIMESTAMP\)`\)/g,'timestamp("updated_at").notNull().defaultNow()').replace(/import { sqliteTable, text, integer } from "drizzle-orm\/sqlite-core";\nimport { sql } from "drizzle-orm";/g,'import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";').replace(/`drizzle-kit push` for SQLite\/Turso/g,"`drizzle-kit push` for Postgres")),u=pr(u),O(s,"AGENTS.md",u),O(s,"CLAUDE.md",u)}let ee=a?.designMd;ee&&O(s,"DESIGN.md",ee),f&&(O(s,"lib/db.ts",['import { neon } from "@neondatabase/serverless";','import { drizzle as drizzleNeon } from "drizzle-orm/neon-http";',"","// eslint-disable-next-line @typescript-eslint/no-explicit-any","let _db: any = null;","","function getDb() {"," if (!_db) {",' if (process.env.DATABASE_URL && process.env.DATABASE_URL !== "pglite") {'," // Production / remote Postgres"," const sql = neon(process.env.DATABASE_URL);"," _db = drizzleNeon(sql);"," } else {"," // Local dev \u2014 PGlite (zero-install embedded Postgres). Lives in"," // lib/db-local.ts and is loaded through a runtime-only require"," // whose path is built from a variable, so esbuild's static"," // analysis can't follow it. Keeps pglite + its 30MB WASM out of"," // the Worker bundle.",' const localPath: string = "./" + "db-local";'," // eslint-disable-next-line @typescript-eslint/no-require-imports"," const { createLocalDb } = require(localPath);"," _db = createLocalDb();"," }"," }"," return _db;","}","","// Lazy proxy \u2014 DB isn't initialized at import/build time","// eslint-disable-next-line @typescript-eslint/no-explicit-any","export const db: any = new Proxy({} as any, {"," get(_target, prop, receiver) {"," const realDb = getDb();"," const value = Reflect.get(realDb, prop, receiver);",' if (typeof value === "function") {'," return value.bind(realDb);"," }"," return value;"," },","});",""].join(`
2422
+ images: {`)))}!y&&u.path.includes("sidebar")&&(g=g.replace(/\{user\.role === "admin"[\s\S]*?<\/Link>\s*\)\}/m,""),g=g.replace(/, Shield/g,"")),O(s,u.path,g)}let P={...S.dependencies},q={...S.devDependencies};if(P["drizzle-zod"]||(P["drizzle-zod"]="^0.5.1"),f&&(delete P["@libsql/client"],P["@neondatabase/serverless"]="^0.10.0",q["@electric-sql/pglite"]="^0.2.0"),h&&(P.stripe="^17.0.0"),m&&(P.resend="^4.0.0",P["@react-email/components"]="^0.0.31"),x&&(P.ai="^4.0.0",P["@ai-sdk/openai"]="^1.0.0",P.openai="^4.0.0"),O(s,"package.json",JSON.stringify({name:r,version:"0.1.0",private:!0,scripts:{dev:"next dev",build:"next build","build:cf":"opennextjs-cloudflare build",start:"next start",lint:"next lint","db:push":"drizzle-kit push","db:studio":"drizzle-kit studio"},dependencies:P,devDependencies:q,optionalDependencies:{"@noble/ciphers":"^1.3.0"},overrides:{react:"19.1.0","react-dom":"19.1.0",punycode:"^2.3.1","zod-to-json-schema":"3.24.6"}},null,2)),S.methodology){let u=S.methodology;f&&(u=u.replace(/sqliteTable/g,"pgTable").replace(/drizzle-orm\/sqlite-core/g,"drizzle-orm/pg-core").replace(/Use `text` for dates \(SQLite stores dates as text\)/g,"Use `timestamp` for dates and `boolean` for booleans (native Postgres types)").replace(/text\("created_at"\)\.notNull\(\)\.default\(sql`\(CURRENT_TIMESTAMP\)`\)/g,'timestamp("created_at").notNull().defaultNow()').replace(/text\("updated_at"\)\.notNull\(\)\.default\(sql`\(CURRENT_TIMESTAMP\)`\)/g,'timestamp("updated_at").notNull().defaultNow()').replace(/import { sqliteTable, text, integer } from "drizzle-orm\/sqlite-core";\nimport { sql } from "drizzle-orm";/g,'import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";').replace(/`drizzle-kit push` for SQLite\/Turso/g,"`drizzle-kit push` for Postgres")),u=pr(u),O(s,"AGENTS.md",u),O(s,"CLAUDE.md",u)}let ee=a?.designMd;ee&&O(s,"DESIGN.md",ee),f&&(O(s,"lib/db.ts",['import { neon } from "@neondatabase/serverless";','import { drizzle as drizzleNeon } from "drizzle-orm/neon-http";',"","// eslint-disable-next-line @typescript-eslint/no-explicit-any","let _db: any = null;","","function getDb() {"," if (!_db) {",' if (process.env.DATABASE_URL && process.env.DATABASE_URL !== "pglite") {'," // Production / remote Postgres"," const sql = neon(process.env.DATABASE_URL);"," _db = drizzleNeon(sql);"," } else {"," // Local dev \u2014 PGlite (zero-install embedded Postgres). Lives in"," // lib/db-local.ts and is loaded through a runtime-only require"," // whose path is built from a variable, so esbuild's static"," // analysis can't follow it. Keeps pglite + its 30MB WASM out of"," // the Worker bundle.",' const localPath: string = "./" + "db-local";'," // eslint-disable-next-line @typescript-eslint/no-require-imports"," const { createLocalDb } = require(localPath);"," _db = createLocalDb();"," }"," }"," return _db;","}","","// Lazy proxy \u2014 DB isn't initialized at import/build time","// eslint-disable-next-line @typescript-eslint/no-explicit-any","export const db: any = new Proxy({} as any, {"," get(_target, prop, receiver) {"," const realDb = getDb();"," const value = Reflect.get(realDb, prop, receiver);",' if (typeof value === "function") {'," return value.bind(realDb);"," }"," return value;"," },","});",""].join(`
2427
2423
  `)),O(s,"lib/db-local.ts",["// Local-dev-only DB factory. Isolated from lib/db.ts so the production","// Cloudflare Worker bundle never loads drizzle-orm/pglite or its","// transitive 30MB WASM binary. Loaded via runtime-only require() from","// db.ts, where the path is built from a variable to defeat static analysis.","",'const pglitePkg: string = ["@electric-sql", "pglite"].join("/");',"// eslint-disable-next-line @typescript-eslint/no-require-imports","const { PGlite } = require(pglitePkg);","",'const drizzlePath: string = "drizzle-orm/" + "pglite";',"// eslint-disable-next-line @typescript-eslint/no-require-imports","const { drizzle } = require(drizzlePath);","","// eslint-disable-next-line @typescript-eslint/no-explicit-any","export function createLocalDb(): any {",' const client = new PGlite("./local.pg");'," return drizzle(client);","}",""].join(`
2428
2424
  `)),O(s,"drizzle.config.ts",['import { defineConfig } from "drizzle-kit";',"","// PGlite for local dev (no Postgres install needed), Mistflow Cloud for production",'const isPglite = !process.env.DATABASE_URL || process.env.DATABASE_URL === "pglite";',"","export default defineConfig({",' schema: "./db/schema",',' out: "./db/migrations",',' dialect: "postgresql",',' ...(isPglite ? { driver: "pglite", dbCredentials: { url: "./local.pg" } } : { dbCredentials: { url: process.env.DATABASE_URL! } }),',"});",""].join(`
2429
2425
  `)),O(s,"db/schema/auth.ts",['import { pgTable, text, boolean, timestamp } from "drizzle-orm/pg-core";',"",'export const user = pgTable("user", {',' id: text("id").primaryKey(),',' name: text("name").notNull(),',' email: text("email").notNull().unique(),',' emailVerified: boolean("email_verified").notNull().default(false),',' image: text("image"),',' role: text("role").default("user"),',' banned: boolean("banned").default(false),',' banReason: text("ban_reason"),',' banExpires: timestamp("ban_expires"),',' createdAt: timestamp("created_at").notNull(),',' updatedAt: timestamp("updated_at").notNull(),',"});","",'export const session = pgTable("session", {',' id: text("id").primaryKey(),',' expiresAt: timestamp("expires_at").notNull(),',' token: text("token").notNull().unique(),',' createdAt: timestamp("created_at").notNull(),',' updatedAt: timestamp("updated_at").notNull(),',' ipAddress: text("ip_address"),',' userAgent: text("user_agent"),',' userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),',' impersonatedBy: text("impersonated_by"),',"});","",'export const account = pgTable("account", {',' id: text("id").primaryKey(),',' accountId: text("account_id").notNull(),',' providerId: text("provider_id").notNull(),',' userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),',' accessToken: text("access_token"),',' refreshToken: text("refresh_token"),',' idToken: text("id_token"),',' accessTokenExpiresAt: timestamp("access_token_expires_at"),',' refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),',' scope: text("scope"),',' password: text("password"),',' createdAt: timestamp("created_at").notNull(),',' updatedAt: timestamp("updated_at").notNull(),',"});","",'export const verification = pgTable("verification", {',' id: text("id").primaryKey(),',' identifier: text("identifier").notNull(),',' value: text("value").notNull(),',' expiresAt: timestamp("expires_at").notNull(),',' createdAt: timestamp("created_at"),',' updatedAt: timestamp("updated_at"),',"});",""].join(`
2430
2426
  `)),O(s,"lib/auth.ts",['import { betterAuth } from "better-auth";','import { drizzleAdapter } from "better-auth/adapters/drizzle";','import { admin } from "better-auth/plugins/admin";','import { nextCookies } from "better-auth/next-js";','import { db } from "./db";','import * as schema from "@/db";',"","async function sendEmail({ to, subject, html, fallbackUrl }: { to: string; subject: string; html: string; fallbackUrl?: string }) {"," const apiKey = process.env.RESEND_API_KEY;"," if (!apiKey) {"," if (fallbackUrl) {"," console.error(`\\n[auth] No RESEND_API_KEY set. Email to ${to} was not sent.`);"," console.error(`[auth] Dev fallback \u2014 use this link directly:\\n ${fallbackUrl}\\n`);"," } else {"," console.error(`[auth] RESEND_API_KEY not set \u2014 skipping email send to ${to}`);"," }"," return;"," }",' const from = process.env.EMAIL_FROM || "noreply@mail.mistflow.app";',' const res = await fetch("https://api.resend.com/emails", {',' method: "POST",',' headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },'," body: JSON.stringify({ from, to, subject, html }),"," });"," if (!res.ok) {",' const body = await res.text().catch(() => "unknown");'," console.error(`[auth] Email send failed (${res.status}): ${body}`);"," throw new Error(`Email send failed: ${res.status}`);"," }","}","","function createAuth() {",' const baseURL = process.env.BETTER_AUTH_URL || process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";',' const isLocal = baseURL.includes("localhost") || baseURL.includes("127.0.0.1");'," const canSendEmail = Boolean(process.env.RESEND_API_KEY);"," // Refuse to boot a production app with email auth but no mail sender. Mistflow's"," // deploy pipeline injects a managed Resend key automatically; if it's missing,"," // that's a real misconfig and silent fallbacks let unverified users sign up."," if (!isLocal && !canSendEmail) {",` throw new Error("[auth] RESEND_API_KEY is required in production. The Mistflow deploy pipeline injects one automatically \u2014 if you're seeing this, check the project's env vars in the dashboard, or set your own RESEND_API_KEY.");`," }"," return betterAuth({"," baseURL,"," trustedOrigins: [baseURL],",' database: drizzleAdapter(db, { provider: "pg", schema }),'," emailAndPassword: {"," enabled: true,"," requireEmailVerification: !isLocal && canSendEmail,"," sendResetPassword: async ({ user, token }: { user: { email: string; name: string }; url: string; token: string }) => {"," // Better Auth's default reset URL points at /reset-password/:token (path),"," // which our frontend doesn't route. We build our own pointing at our"," // /reset-password?token=xxx page which reads the token from the query."," const resetUrl = `${baseURL}/reset-password?token=${token}`;"," await sendEmail({"," to: user.email,",' subject: "Reset your password",',' html: `<p>Hi ${user.name},</p><p>Click the link below to reset your password:</p><p><a href="${resetUrl}">${resetUrl}</a></p>`,'," fallbackUrl: isLocal ? resetUrl : undefined,"," });"," },"," },"," emailVerification: {"," sendOnSignUp: canSendEmail,"," autoSignInAfterVerification: true,"," sendVerificationEmail: async ({ user, url }: { user: { email: string; name: string }; url: string }) => {"," await sendEmail({"," to: user.email,",' subject: "Verify your email address",',' html: `<p>Hi ${user.name},</p><p>Click the link below to verify your email:</p><p><a href="${url}">${url}</a></p>`,'," fallbackUrl: isLocal ? url : undefined,"," });"," },"," },"," secret: process.env.AUTH_SECRET,",' plugins: [admin({ defaultRole: "user" }), nextCookies()],'," databaseHooks: {"," user: {"," create: {"," // Auto-promote the app owner to admin on first signup. ADMIN_EMAIL"," // is injected by the Mistflow deploy pipeline (the email of the"," // account that ran mist_deploy). Email verification still gates"," // login when Resend is configured, so a collision attempt can't"," // actually sign in without clicking a link delivered to the"," // owner's inbox."," before: async (user: { email?: string; [k: string]: unknown }) => {"," const adminEmail = process.env.ADMIN_EMAIL;"," if (adminEmail && user.email?.toLowerCase() === adminEmail.toLowerCase()) {",' return { data: { ...user, role: "admin" } };'," }"," return { data: user };"," },"," },"," },"," },"," socialProviders: {"," ...(process.env.GOOGLE_CLIENT_ID ? {"," google: {"," clientId: process.env.GOOGLE_CLIENT_ID,"," clientSecret: process.env.GOOGLE_CLIENT_SECRET!,"," },"," } : {}),"," ...(process.env.GITHUB_CLIENT_ID ? {"," github: {"," clientId: process.env.GITHUB_CLIENT_ID,"," clientSecret: process.env.GITHUB_CLIENT_SECRET!,"," },"," } : {}),"," },"," });","}","","// Lazy init \u2014 process.env isn't populated at module scope on Cloudflare Workers.","// The `has` trap is required: better-auth's toNextJsHandler does",'// `"handler" in auth ? auth.handler(request) : auth(request)` \u2014 without a `has`',"// trap the default forwards to the empty target object, returns false, and the","// handler tries to call the Proxy as a function, which throws TypeError and","// returns 500 on every /api/auth/* request.","let _auth: ReturnType<typeof createAuth> | null = null;","export const auth = new Proxy({} as ReturnType<typeof createAuth>, {"," get(_target, prop, receiver) {"," if (!_auth) _auth = createAuth();"," const value = Reflect.get(_auth, prop, receiver);",' if (typeof value === "function") return value.bind(_auth);'," return value;"," },"," has(_target, prop) {"," if (!_auth) _auth = createAuth();"," return prop in _auth;"," },","});",""].join(`
2431
- `)))}else O(s,"package.json",JSON.stringify({name:r,version:"0.1.0",private:!0},null,2));let A=a?.designMd;O(s,"app/globals.css",cl(l,d,A)),O(s,"app/layout.tsx",ul(r,l,b?.language)),O(s,"README.md",Sl(r,a,{hasStripe:h,hasResend:m,hasStorage:p,hasAdmin:y,hasAI:x,isNeon:f})),O(s,"contracts/README.md",dr());let E=a?.dataModel??[],T=new Set,j=0;for(let w of E){let I=w.entity??w.name;if(!I||typeof I!="string")continue;let q=mr(I);T.has(q)||(T.add(q),O(s,q,ur(I)),j++)}j===0&&O(s,"contracts/.gitkeep","");let L=[],v=a?.publicPages;if(Array.isArray(v))L=v;else if(typeof v=="string"){try{L=JSON.parse(v)}catch{L=[]}Array.isArray(L)||(L=[])}if(!L.includes("/")){let w=a?.steps?.some(q=>{let K=((q.name??"")+" "+(q.description??"")).toLowerCase();return K.includes("landing")||K.includes("marketing")||K.includes("homepage")}),I=a?.pages?.some(q=>q.path==="/");(w||I)&&(L=["/",...L])}let M={name:r,summary:a?.summary,authModel:a?.authModel,roles:a?.roles,defaultRole:a?.defaultRole,publicPages:L,navStyle:a?.navStyle,multiTenant:a?.multiTenant,pages:a?.pages,dataModel:a?.dataModel,design:a?.design},H=hl(M);H&&O(s,"middleware.ts",H);let W=gl(M);W&&O(s,W.path,W.content);let oe=fl(M);if(oe&&O(s,"lib/roles.ts",oe),O(s,"app/page.tsx",yl(M)),O(s,"app/(dashboard)/layout.tsx",bl(M,y)),O(s,"app/(dashboard)/dashboard/page.tsx",wl(M)),M.multiTenant){let w=vl(M,f);w&&O(s,"db/schema/organization.ts",w);let I=xl(M);I&&O(s,"lib/org.ts",I);let q=kl(M);q&&O(s,"components/org-switcher.tsx",q)}O(s,"tsconfig.json",JSON.stringify({compilerOptions:{target:"ES2017",lib:["dom","dom.iterable","esnext"],allowJs:!0,skipLibCheck:!0,strict:!1,noEmit:!0,esModuleInterop:!0,module:"esnext",moduleResolution:"bundler",resolveJsonModule:!0,isolatedModules:!0,jsx:"preserve",incremental:!0,plugins:[{name:"next"}],paths:{"@/*":["./*"]}},include:["next-env.d.ts","**/*.ts","**/*.tsx",".next/types/**/*.ts"],exclude:["node_modules"]},null,2)),h&&O(s,"lib/stripe.ts",['import Stripe from "stripe";',"","let _stripe: Stripe | null = null;","","function getStripe(): Stripe {"," if (!_stripe) {"," if (!process.env.STRIPE_SECRET_KEY) {",' throw new Error("STRIPE_SECRET_KEY is not set");'," }"," _stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {"," typescript: true,"," });"," }"," return _stripe;","}","","// Lazy proxy \u2014 Stripe isn't initialized at import/build time","export const stripe = new Proxy({} as Stripe, {"," get(_target, prop) {"," return (getStripe() as unknown as Record<string | symbol, unknown>)[prop];"," },","});",""].join(`
2427
+ `)))}else O(s,"package.json",JSON.stringify({name:r,version:"0.1.0",private:!0},null,2));let A=a?.designMd;O(s,"app/globals.css",cl(l,d,A)),O(s,"app/layout.tsx",ul(r,l,b?.language)),O(s,"README.md",Sl(r,a,{hasStripe:h,hasResend:m,hasStorage:p,hasAdmin:y,hasAI:x,isNeon:f})),O(s,"contracts/README.md",dr());let E=a?.dataModel??[],T=new Set,j=0;for(let w of E){let P=w.entity??w.name;if(!P||typeof P!="string")continue;let q=mr(P);T.has(q)||(T.add(q),O(s,q,ur(P)),j++)}j===0&&O(s,"contracts/.gitkeep","");let L=[],v=a?.publicPages;if(Array.isArray(v))L=v;else if(typeof v=="string"){try{L=JSON.parse(v)}catch{L=[]}Array.isArray(L)||(L=[])}if(!L.includes("/")){let w=a?.steps?.some(q=>{let K=((q.name??"")+" "+(q.description??"")).toLowerCase();return K.includes("landing")||K.includes("marketing")||K.includes("homepage")}),P=a?.pages?.some(q=>q.path==="/");(w||P)&&(L=["/",...L])}let M={name:r,summary:a?.summary,authModel:a?.authModel,roles:a?.roles,defaultRole:a?.defaultRole,publicPages:L,navStyle:a?.navStyle,multiTenant:a?.multiTenant,pages:a?.pages,dataModel:a?.dataModel,design:a?.design},H=hl(M);H&&O(s,"middleware.ts",H);let W=gl(M);W&&O(s,W.path,W.content);let re=fl(M);if(re&&O(s,"lib/roles.ts",re),O(s,"app/page.tsx",yl(M)),O(s,"app/(dashboard)/layout.tsx",bl(M,y)),O(s,"app/(dashboard)/dashboard/page.tsx",wl(M)),M.multiTenant){let w=vl(M,f);w&&O(s,"db/schema/organization.ts",w);let P=xl(M);P&&O(s,"lib/org.ts",P);let q=kl(M);q&&O(s,"components/org-switcher.tsx",q)}O(s,"tsconfig.json",JSON.stringify({compilerOptions:{target:"ES2017",lib:["dom","dom.iterable","esnext"],allowJs:!0,skipLibCheck:!0,strict:!1,noEmit:!0,esModuleInterop:!0,module:"esnext",moduleResolution:"bundler",resolveJsonModule:!0,isolatedModules:!0,jsx:"preserve",incremental:!0,plugins:[{name:"next"}],paths:{"@/*":["./*"]}},include:["next-env.d.ts","**/*.ts","**/*.tsx",".next/types/**/*.ts"],exclude:["node_modules"]},null,2)),h&&O(s,"lib/stripe.ts",['import Stripe from "stripe";',"","let _stripe: Stripe | null = null;","","function getStripe(): Stripe {"," if (!_stripe) {"," if (!process.env.STRIPE_SECRET_KEY) {",' throw new Error("STRIPE_SECRET_KEY is not set");'," }"," _stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {"," typescript: true,"," });"," }"," return _stripe;","}","","// Lazy proxy \u2014 Stripe isn't initialized at import/build time","export const stripe = new Proxy({} as Stripe, {"," get(_target, prop) {"," return (getStripe() as unknown as Record<string | symbol, unknown>)[prop];"," },","});",""].join(`
2432
2428
  `)),m&&(O(s,"lib/resend.ts",['import { Resend } from "resend";',"","let _resend: Resend | null = null;","","function getResend(): Resend {"," if (!_resend) {"," if (!process.env.RESEND_API_KEY) {",' throw new Error("RESEND_API_KEY is not set");'," }"," _resend = new Resend(process.env.RESEND_API_KEY);"," }"," return _resend;","}","","// Lazy proxy \u2014 Resend isn't initialized at import/build time","export const resend = new Proxy({} as Resend, {"," get(_target, prop) {"," return (getResend() as unknown as Record<string | symbol, unknown>)[prop];"," },","});",""].join(`
2433
2429
  `)),O(s,"lib/email.ts",['import { resend } from "./resend";',"",'const FROM = process.env.EMAIL_FROM ?? "onboarding@resend.dev";',"","export async function sendEmail({"," to,"," subject,"," react,","}: {"," to: string;"," subject: string;"," react: React.ReactElement;","}) {"," return resend.emails.send({ from: FROM, to, subject, react });","}",""].join(`
2434
2430
  `))),p&&(O(s,"lib/storage.ts",['const MISTFLOW_API = process.env.MISTFLOW_API_URL ?? "https://api.mistflow.ai";',"const MISTFLOW_API_KEY = process.env.MISTFLOW_API_KEY;","const PROJECT_ID = process.env.MISTFLOW_PROJECT_ID;","","interface UploadResult {"," upload_url: string;"," download_url: string;"," key: string;","}","","function authHeaders(): Record<string, string> {"," return {",' "Content-Type": "application/json",',' "Authorization": `ApiKey ${MISTFLOW_API_KEY}`,'," };","}","",'export async function getUploadUrl(filename: string, contentType: string = "application/octet-stream"): Promise<UploadResult> {'," const res = await fetch(`${MISTFLOW_API}/api/storage/upload-url`, {",' method: "POST",'," headers: authHeaders(),"," body: JSON.stringify({ project_id: PROJECT_ID, filename, content_type: contentType }),"," });"," if (!res.ok) throw new Error(`Storage error: ${res.status}`);"," return res.json();","}","","export async function getDownloadUrl(filename: string): Promise<string> {"," const res = await fetch(`${MISTFLOW_API}/api/storage/download-url`, {",' method: "POST",'," headers: authHeaders(),"," body: JSON.stringify({ project_id: PROJECT_ID, filename }),"," });"," if (!res.ok) throw new Error(`Storage error: ${res.status}`);"," const data = await res.json();"," return data.download_url;","}","","export async function deleteFile(filename: string): Promise<void> {"," await fetch(`${MISTFLOW_API}/api/storage/delete`, {",' method: "POST",'," headers: authHeaders(),"," body: JSON.stringify({ project_id: PROJECT_ID, filename }),"," });","}","","export async function uploadFile(file: File): Promise<string> {"," const { upload_url, download_url } = await getUploadUrl(file.name, file.type);",' await fetch(upload_url, { method: "PUT", body: file, headers: { "Content-Type": file.type } });'," return download_url;","}",""].join(`
2435
2431
  `)),O(s,"app/api/upload/route.ts",['import { NextRequest, NextResponse } from "next/server";','import { getUploadUrl } from "@/lib/storage";',"","export async function POST(req: NextRequest) {"," const { filename, contentType } = await req.json();"," if (!filename) {",' return NextResponse.json({ error: "filename is required" }, { status: 400 });'," }"," try {",' const result = await getUploadUrl(filename, contentType ?? "application/octet-stream");'," return NextResponse.json(result);"," } catch {",' return NextResponse.json({ error: "Failed to get upload URL" }, { status: 500 });'," }","}",""].join(`
2436
2432
  `))),x&&(O(s,"lib/ai.ts",['import { createOpenAI } from "@ai-sdk/openai";',"","export const openai = createOpenAI({"," apiKey: process.env.OPENAI_API_KEY,","});",""].join(`
2437
2433
  `)),O(s,"app/api/chat/route.ts",['import { openai } from "@/lib/ai";','import { streamText } from "ai";',"","export async function POST(req: Request) {"," const { messages } = await req.json();",""," const result = streamText({",' model: openai("gpt-4o"),'," messages,"," });",""," return result.toDataStreamResponse();","}",""].join(`
2438
- `)));let N=Array.isArray(a?.integrations)?a.integrations.map(w=>({name:w.name,preset:w.preset,envVars:w.envVars??[]})):[],_={};x&&!N.some(w=>w.envVars?.some(I=>I.key==="OPENAI_API_KEY"))&&(_.OPENAI_API_KEY={description:"OpenAI API key",setupUrl:"https://platform.openai.com/api-keys"});for(let w of N)for(let I of w.envVars??[])_[I.key]||(_[I.key]={description:I.description,setupUrl:I.setupUrl,...w.name?{integration:w.name}:{}});let re={name:r,methodologyVersion:S?.version??"1.0",createdAt:new Date().toISOString(),...i?{planId:i}:{},plan:Array.isArray(a?.steps)?{...a,steps:a.steps.map(w=>({number:w.number,name:w.name??w.title,description:w.description,entities:w.entities,pages:w.pages,features:w.features,status:"pending"}))}:a,dbProvider:"neon",env:{managed:{DATABASE_URL:{description:"Postgres connection URL",scope:"production"},AUTH_SECRET:{description:"Auth encryption secret",scope:"production"},...h?{STRIPE_SECRET_KEY:{description:"Stripe secret key",scope:"production"},STRIPE_WEBHOOK_SECRET:{description:"Stripe webhook signing secret",scope:"production"},NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY:{description:"Stripe publishable key",scope:"production"}}:{},...m?{RESEND_API_KEY:{description:"Resend API key \u2014 managed by Mistflow by default, override with your own key from resend.com",scope:"production"},EMAIL_FROM:{description:"Sender email address \u2014 managed by Mistflow by default",scope:"production"}}:{},...p?{MISTFLOW_API_KEY:{description:"Mistflow API key for file storage",scope:"production"},MISTFLOW_PROJECT_ID:{description:"Mistflow project ID",scope:"production"}}:{}},...Object.keys(_).length>0?{required:_}:{}},authModel:a?.authModel??"email",roles:a?.roles??null,navStyle:a?.navStyle??"sidebar",multiTenant:a?.multiTenant??!1,hasAdmin:y,hasResend:m,hasStorage:p,hasAI:x,deploy:null};O(s,"mistflow.json",JSON.stringify(re,null,2));let ce=Za(32).toString("hex"),D=h?`
2434
+ `)));let N=Array.isArray(a?.integrations)?a.integrations.map(w=>({name:w.name,preset:w.preset,envVars:w.envVars??[]})):[],_={};x&&!N.some(w=>w.envVars?.some(P=>P.key==="OPENAI_API_KEY"))&&(_.OPENAI_API_KEY={description:"OpenAI API key",setupUrl:"https://platform.openai.com/api-keys"});for(let w of N)for(let P of w.envVars??[])_[P.key]||(_[P.key]={description:P.description,setupUrl:P.setupUrl,...w.name?{integration:w.name}:{}});let ne={name:r,methodologyVersion:S?.version??"1.0",createdAt:new Date().toISOString(),...i?{planId:i}:{},plan:Array.isArray(a?.steps)?{...a,steps:a.steps.map(w=>({number:w.number,name:w.name??w.title,description:w.description,entities:w.entities,pages:w.pages,features:w.features,status:"pending"}))}:a,dbProvider:"neon",env:{managed:{DATABASE_URL:{description:"Postgres connection URL",scope:"production"},AUTH_SECRET:{description:"Auth encryption secret",scope:"production"},...h?{STRIPE_SECRET_KEY:{description:"Stripe secret key",scope:"production"},STRIPE_WEBHOOK_SECRET:{description:"Stripe webhook signing secret",scope:"production"},NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY:{description:"Stripe publishable key",scope:"production"}}:{},...m?{RESEND_API_KEY:{description:"Resend API key \u2014 managed by Mistflow by default, override with your own key from resend.com",scope:"production"},EMAIL_FROM:{description:"Sender email address \u2014 managed by Mistflow by default",scope:"production"}}:{},...p?{MISTFLOW_API_KEY:{description:"Mistflow API key for file storage",scope:"production"},MISTFLOW_PROJECT_ID:{description:"Mistflow project ID",scope:"production"}}:{}},...Object.keys(_).length>0?{required:_}:{}},authModel:a?.authModel??"email",roles:a?.roles??null,navStyle:a?.navStyle??"sidebar",multiTenant:a?.multiTenant??!1,hasAdmin:y,hasResend:m,hasStorage:p,hasAI:x,deploy:null};O(s,"mistflow.json",JSON.stringify(ne,null,2));let ce=Za(32).toString("hex"),D=h?`
2439
2435
  # Stripe
2440
2436
  STRIPE_SECRET_KEY=
2441
2437
  STRIPE_WEBHOOK_SECRET=
@@ -2459,8 +2455,8 @@ OPENAI_API_KEY=
2459
2455
  AUTH_SECRET=${ce}
2460
2456
  ${D}${Q}${Se}${ue}`),O(s,".env.example",`${$e}
2461
2457
  AUTH_SECRET=your-secret-here
2462
- ${D}${Q}${Se}${ue}`);let F=[],R=(w,I)=>{F.push({phase:w,message:I})},z=(w,I)=>{let q=F.find(K=>K.phase===w&&!K.durationMs);q&&(q.durationMs=I)};if(e){let w=eo(e.server,e.progressToken,()=>F[F.length-1]?.message??"Setting up project...");e.cleanup=()=>w.stop()}let ge=b?.requestedSubdomain||void 0,G,ke;R("register","Registering project on Mistflow...");let Fe=Date.now();try{let w=await xt(r,void 0,"neon",ge);G=w.id;let I=ve(s,"mistflow.json"),q=JSON.parse(so(I,"utf-8"));if(q.projectId=G,gr(I,JSON.stringify(q,null,2)),St(s,Tt(G,r)),w.managed_env&&Object.keys(w.managed_env).length>0){let K=ve(s,".env.local"),fe=Ve(K)?so(K,"utf-8"):"";for(let[ee,u]of Object.entries(w.managed_env)){let g=new RegExp(`^${ee}=.*$`,"m");g.test(fe)?fe=fe.replace(g,`${ee}=${u}`):fe+=`
2463
- ${ee}=${u}`}gr(K,fe)}try{let{getBaseUrl:K,getAuthHeaders:fe}=await Promise.resolve().then(()=>(pe(),Kr)),ee=fe(),u=a?.features,g=a?.steps,k={};Array.isArray(u)&&u.length>0&&(k.features=u.map(P=>P.name)),a&&(k.plan=a),Array.isArray(g)&&g.length>0&&(k.provenance=g.map(P=>({feature:P.name??P.title??`Step ${P.number??"?"}`,user_intent:(P.description??"").slice(0,500),decisions:"Seeded from plan at init",tradeoffs:"",files_affected:[]}))),Object.keys(k).length>0&&await fetch(`${K()}/api/projects/${encodeURIComponent(G)}/state`,{method:"PUT",headers:{...ee,"Content-Type":"application/json"},body:JSON.stringify(k)})}catch{}F[F.length-1].message=`Registered as ${G.slice(0,8)}`}catch(w){let I=w instanceof Error?w.message:String(w);console.error("Could not register project on backend:",I),ke=`Project created locally but NOT registered on Mistflow servers (${I}). Deploy will auto-register it.`,F[F.length-1].message="Registration skipped (offline \u2014 deploy will retry)"}z("register",Date.now()-Fe),R("git","Initializing git repository...");let qe=Date.now();try{let w=tl(s);await w.init(),await w.add("."),await w.commit("Initial Mistflow project setup"),F[F.length-1].message="Git repository initialized"}catch{console.error("Git initialization failed, continuing without git."),F[F.length-1].message="Git init skipped"}z("git",Date.now()-qe);let Be=F.reduce((w,I)=>w+(I.durationMs??0),0),me={projectPath:s,projectId:G,status:"awaiting_install"},_e=F.map(w=>{let I=w.durationMs?` (${(w.durationMs/1e3).toFixed(1)}s)`:"";return`${w.message}${I}`});me.progress=_e,me.totalSetupTime=`${(Be/1e3).toFixed(1)}s`;let Re=[];G||Re.push("Project was not registered with Mistflow (not signed in). Run mist_setup to sign in BEFORE deploying \u2014 deploy will fail without it."),ke&&(me.registrationWarning=ke),Re.length>0&&(me.warnings=Re);let Ne=`NEXT: Call mist_install({ projectPath: "${s}" }). This is fire-and-poll \u2014 the first call returns a jobId and status: "running"; re-call with the same jobId every ~15-30s until status is "complete". Typical duration 30-90s. Do NOT ask the user for permission \u2014 install is a required follow-up to init, not a decision point. After install completes, call mist_implement({ projectPath: "${s}" }) to build the first plan step.`;return me.nextAction=G?Ne:`${Ne} IMPORTANT: You MUST also run mist_setup to sign in before deploying \u2014 the project could not be registered because auth is missing.`,c(JSON.stringify(me))}var Gn={name:"mist_init",description:"Initialize a new Next.js project using the Mistflow stack. Downloads templates from the Mistflow registry, installs dependencies, sets up auth, and initializes git. Use this after mist_plan to create the project.",inputSchema:rl,handler:Tl};import{z as yr}from"zod";import{existsSync as Nt,readFileSync as wr,writeFileSync as br,mkdirSync as Hl}from"fs";import{join as dt,resolve as Wl,dirname as Gl}from"path";pe();import{createConnection as Vl}from"net";var Vn="\nApply these choices to every file you create. Customize the shadcn CSS variables in globals.css to match. Do NOT use default shadcn blue/zinc theme.\n\n**CRITICAL: shadcn/ui components are already installed. NEVER write components/ui/*.tsx files from scratch.** If you need a shadcn component, run `npx shadcn@latest add <component>` to pull it. The project already includes: button, card, input, label, form, dialog, table, dropdown-menu, badge, separator, skeleton, sheet, tabs, avatar, select, textarea, checkbox, switch, tooltip, popover, sonner.\n\n**shadcn MCP server is available.** You have the `shadcn` MCP server installed \u2014 use it to browse and search for components, blocks, and landing page sections from the shadcn registry. Before building a landing page section from scratch, check the registry for existing blocks (hero sections, feature grids, pricing tables, testimonials, footers) and customize them instead of reinventing.\n\n**Project routes (use these exact paths):** Login is at `/login` (NOT /sign-in). Register is at `/register` (NOT /sign-up). All landing page and nav links MUST use `/login` and `/register`.\n\n### Design quality rules (non-negotiable):\n\n**Typography**: Use the plan fonts everywhere. font-heading for headings, font-body for body text.\n- **Modular scale**: Use a 1.25-1.5 ratio between sizes. A 5-level system covers most needs: xs (0.75rem captions), sm (0.875rem metadata), base (1rem body), lg (1.25-1.5rem subheadings), xl+ (2-4rem headlines). Sizes too close together (14/15/16px) create muddy hierarchy.\n- **Vertical rhythm**: Your line-height IS your spacing unit. If body is 16px with line-height 1.5 (= 24px), vertical spacing should be multiples of 24px.\n- **Weight contrast**: Use font-medium/font-semibold contrast, not just size. Bold headings + regular body creates natural hierarchy without extra sizes.\n- **Measure**: Set `max-width: 65ch` on text containers. Long lines kill readability.\n- **OpenType polish**: Use `font-variant-numeric: tabular-nums` on data tables for aligned columns.\n- Never use more than 2 font families. One well-chosen family in multiple weights is cleaner than two competing typefaces.\n- Never fall back to system fonts. Never use decorative fonts for body text. Minimum 16px (1rem) for body.\n\n**Color**: Build a functional palette, not a rainbow.\n- **Tinted neutrals**: Pure gray is dead. Add a tiny tint toward the accent color to all your grays (borders, backgrounds, muted text). The tint should be subtle enough not to read as 'colored' but creates subconscious cohesion.\n- **60/30/10 rule**: 60% neutral backgrounds/whitespace, 30% secondary colors (text, borders, inactive states), 10% accent (CTAs, highlights, focus). Accent colors work BECAUSE they're rare. Overuse kills their power.\n- **Dangerous combos to avoid**: Light gray text on white (#1 accessibility fail). Gray text on any colored background (looks washed out and dead, use a darker shade of the background color instead). Yellow text on white. Thin light text on images.\n- **Dark mode is not inverted light mode**: Use lighter surfaces for depth (no shadows). Desaturate accents slightly. Never pure black backgrounds, use dark gray (12-18% lightness). Reduce body text weight slightly (350 vs 400) because light-on-dark reads heavier.\n- Never use pure #000 or #fff. Customize the shadcn CSS variables in globals.css to match the plan palette.\n\n**Layout & space**: Space is a design material, not leftover.\n- **4px base unit**: All spacing should be multiples of 4: 4, 8, 12, 16, 24, 32, 48, 64, 96px. No arbitrary values. Name tokens semantically (`space-sm`, `space-lg`), not by value.\n- **Use `gap` not margins**: Use `gap` for sibling spacing. It eliminates margin collapse hacks and is more predictable. Reserve margin for positioning, not spacing between siblings.\n- **Rhythm through contrast**: Tight groupings within related items (8-12px), generous separation between sections (48-96px). Vary spacing WITHIN sections too. Not every row needs the same gap. The ratio between inner and outer spacing creates hierarchy.\n- **Density matches content**: Data-dense UIs (dashboards, tables, admin panels) need tighter spacing with more information visible. Marketing pages need generous whitespace and breathing room. Match density to what the user is doing.\n- **The squint test**: Blur your eyes. Can you still identify primary, secondary, and clear groupings? If everything looks the same weight, hierarchy is broken. The most important content should be obvious within 2 seconds.\n- **Asymmetry > centering**: Left-aligned with asymmetric layouts feels more designed. Center-alignment is fine for hero sections and CTAs, not for body content.\n- **Flex vs Grid**: Use Flexbox for 1D layouts (rows of items, navbars, button groups, card contents, most component internals). Use Grid for 2D layouts (page-level structure, dashboards, data-dense interfaces where rows AND columns need coordinated control). Don't default to Grid when Flexbox with `flex-wrap` would be simpler. Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Use named `grid-template-areas` for complex page layouts and redefine at breakpoints.\n- **Cards earn their existence**: Only use cards when content needs clear separation, grid comparison, or interaction boundaries. Not everything needs a container. Never nest cards inside cards. Vary card sizes or mix cards with non-card content to break repetition.\n- **Section variety**: Don't make every section the same structure. Alternate layouts (text-left/image-right, then image-left/text-right), vary section heights, mix full-width with contained content. Monotonous section rhythm signals no designer touched this.\n- **Z-index scale**: Use a semantic scale, not arbitrary numbers. dropdown(10) -> sticky(20) -> modal-backdrop(30) -> modal(40) -> toast(50) -> tooltip(60). Never use 999 or 9999.\n- **Touch targets**: Minimum 44x44px for all interactive elements, even if the visual element is smaller (use padding).\n\n**Cards & surfaces**: Match the plan's cardStyle. Stat cards need background fills with the accent color as a subtle icon background or tinted left border. Use bg-muted/30 for section differentiation between page areas. Build a consistent shadow scale (sm -> md -> lg) and use elevation to reinforce hierarchy, not as decoration.\n\n**Sidebar**: App name + icon at top. Nav items with hover:bg-muted rounded-md transition. Active item: bg-primary/10 text-primary font-medium. User email + sign out at bottom. The sidebar should feel like a distinct surface (bg-card or bg-muted/50).\n**Mobile navigation**: The desktop sidebar must collapse to a hamburger menu on mobile. Use the shadcn `Sheet` component: a hamburger icon button (visible at `md:hidden`) opens a Sheet from the left containing the same nav items. The desktop sidebar is `hidden md:flex`. For simple apps with 3-5 nav items, a bottom tab bar (`fixed bottom-0`) is an alternative. Always respect `env(safe-area-inset-bottom)` for notched phones.\n\n**Interaction design**: Every interactive element needs 8 states, not just default and hover.\n- **Required states**: default, hover, focus, active/pressed, disabled, loading, error, success. Design all of them intentionally.\n- **Focus rings**: Use `:focus-visible` (not `:focus`) so rings show for keyboard users but not mouse clicks. Never remove focus indicators without replacement.\n- **Button hierarchy**: Don't make every button primary. Use ghost, outline, secondary, and text-link variants. One primary CTA per view.\n- **Progressive disclosure**: Simple first, advanced behind expandable sections. Don't overwhelm with options.\n- **Undo over confirmation**: For destructive actions, remove immediately + show an undo toast, then permanently delete after timeout. Better than confirmation dialogs that users click through blindly.\n- **Form patterns**: Visible `<label>` elements always (placeholders aren't labels, they disappear). Validate on blur, not on keystroke. Show format expectations with examples.\n\n**Empty states**: Every empty list/table needs a rich empty state. Not just 'Nothing here'. Include: a simple inline SVG illustration (48x48, relevant to the feature), a specific message that teaches ('Create your first habit to start tracking your daily routine'), and a primary action button. Empty states are the first thing new users see. Make them inviting.\n\n**Loading states**: Use the shadcn `Skeleton` component. Show skeleton versions of cards, lists, and stats during data fetches. Example: `<Skeleton className=\"h-8 w-32\" />` for a stat number, `<Skeleton className=\"h-20 w-full\" />` for a card. Add a `loading.tsx` file in dashboard route groups. Never show a blank page or a lone spinner.\n\n**Images**: For Unsplash images on landing pages, use `next/image` with `placeholder=\"blur\"` and a blurDataURL (a tiny 10x6 base64 image \u2014 generate a solid color blur like `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAGCAYAAAD68A/GAAAAP0lEQVQYV2N89+7dfwYGBgZGRkYGBgYmBjIBE7kaPHv27D8DA8N/BgYGRkZGRgYGJgYyARO5GkjWQHEoxRoAAPyTGAGBMpEAAAAASUVORK5CYII=`). This creates a smooth shimmer-in effect instead of images popping in.\n\n**Motion** (`motion` package is installed):\n\n**Hero moment strategy**: Pick ONE signature animation per page. Not scattered animations on everything. On a landing page, the hero entrance is your hero moment. On a dashboard, a satisfying success animation after the primary action. One great animation > ten mediocre ones. Everything else gets subtle transitions.\n\n**Animation opportunity audit** -- check these 5 categories:\n1. **Missing feedback**: Button has no press response? Add `active:scale-[0.97]` (CSS) or `whileTap={{ scale: 0.97 }}` (motion). Toggle has no slide? Animate it.\n2. **Jarring transitions**: Menu appears instantly? Add 200ms fade + slide. Content swaps without transition? Crossfade between states.\n3. **Unclear relationships**: Parent-child not visually connected? Stagger children 50ms after parent. Deleted item just disappears? Animate it out (opacity + height collapse).\n4. **Lack of delight**: Success state is flat? Draw a checkmark SVG with stroke-dashoffset animation. Empty state is static? Add a gentle float on the illustration.\n5. **Missed guidance**: First-time user gets no hint? Pulse the primary CTA once on page load. New feature unnoticed? Brief highlight animation.\n\n**Timing hierarchy**: 100-150ms for instant feedback (buttons, toggles). 200-300ms for state changes (menus, tooltips). 300-500ms for layout shifts (accordions, modals). 500-800ms for entrances (page loads). Exit animations run at 75% of entrance duration.\n\n**Easing**: Use exponential curves (Quart out, Expo out) for sophisticated physics. Never `ease` (generic), never bounce/elastic (dated). Real objects decelerate smoothly. CSS: `cubic-bezier(0.25, 1, 0.5, 1)` (Quart out) or `cubic-bezier(0.16, 1, 0.3, 1)` (Expo out).\n\n**Micro-interaction recipes**:\n- **Button press**: `active:scale-[0.97] transition-transform duration-100` (CSS only, no motion needed)\n- **Success checkmark**: SVG `<path>` with `stroke-dasharray` + `stroke-dashoffset` animation from full to 0. 400ms Expo out.\n- **Height expand/collapse**: `grid-template-rows: 0fr -> 1fr` with `transition: grid-template-rows 300ms`. No JS layout measurement needed.\n- **Skeleton to content**: Skeleton pulses (CSS `animate-pulse`), then crossfades to real content with `opacity` transition. Use the pre-scaffolded `<ContentLoader>` component.\n- **Staggered list entrance**: CSS `animation-delay` on children: `.animate-fade-up:nth-child(1) { animation-delay: 0ms } :nth-child(2) { animation-delay: 50ms }` etc. Or use `<StaggerList>` component.\n- **Number count-up**: Use `@number-flow/react` for smooth digit morphing on stats/metrics.\n\n**Performance**: Only animate `transform` and `opacity` (GPU-accelerated). Never animate width, height, top, left, margin, or padding. For scroll-tied effects on landing pages, use CSS `animation-timeline: scroll()` where supported (progressive enhancement).\n\n**Accessibility**: Always respect `prefers-reduced-motion`. Wrap animations in `@media (prefers-reduced-motion: no-preference) { ... }`. Provide static alternatives that convey the same information.\n\n**SSR-safe (CRITICAL)**: This app deploys to Mistflow Cloud's edge runtime where IntersectionObserver may not fire. NEVER use `initial={{ opacity: 0 }}` (content stays invisible forever). Use CSS @keyframes for entrance effects:\n```tsx\n// SAFE: CSS @keyframes in globals.css:\n// @keyframes fade-up { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }\n// .animate-fade-up { animation: fade-up 0.5s ease-out both; }\n// Use motion ONLY for hover/tap/gesture interactions:\nimport { motion } from 'motion/react';\n<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.97 }} transition={{ duration: 0.15 }}>\n// NEVER: <motion.div initial={{ opacity: 0 }} whileInView={{ opacity: 1 }}>\n```\nFor dashboard pages, keep animations minimal. Button press feedback + skeleton loading + success states. No entrance animations on dashboard pages.\n\n**UX writing**: Every word earns its place.\n- **Buttons**: Use outcome-focused verb + object ('Save changes', 'Create account'), never generic ('Submit', 'OK', 'Click here').\n- **Destructive actions**: State exact consequences ('Delete 5 items permanently', not 'Delete selected'). Distinguish 'Delete' (permanent) from 'Remove' (recoverable).\n- **Error messages**: Answer three questions: what happened, why, and how to fix it. 'Email address is not valid. Include an @ symbol.' not 'Invalid input'. Never blame the user.\n- **Empty states**: Explain what will appear here, why it matters, and give a clear next action.\n- **Loading/progress**: Set time expectations. 'Deploying your app (usually takes 30 seconds)' not just a spinner.\n- **Consistency**: Pick one term and stick with it. 'Delete' everywhere, not Delete/Remove/Trash interchangeably.\n- **Link text**: Make it standalone for accessibility. 'View pricing plans' not 'Click here'.\n\n**Responsive design**: Mobile-first, then enhance.\n- **Mobile-first CSS**: Write base styles for mobile, then use `min-width` media queries to enhance for larger screens. Never start with desktop and try to cram it into mobile.\n- **Input method matters more than screen size**: Use `@media (pointer: fine)` for mouse/trackpad (smaller targets OK, hover effects work), `@media (pointer: coarse)` for touch (44px targets required, no hover-dependent UI). Use `@media (hover: none)` to detect devices that can't hover. Never require hover to reveal functionality.\n- **Safe areas**: Add `<meta name='viewport' content='width=device-width, initial-scale=1, viewport-fit=cover'>` and use `env(safe-area-inset-bottom)` for bottom navigation on notched phones. Fixed bottom bars must respect safe areas.\n- **Self-adjusting grids**: Use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for card grids. Columns reflow naturally without breakpoint management.\n- **Container queries over media queries**: For reusable components, use `container-type: inline-size` on the parent and `@container (min-width: 400px)` on children. Components adapt to their container, not the viewport.\n- **No horizontal scroll**: Nothing should overflow the viewport width. Test at 320px width (smallest common phone). Use `overflow-x: hidden` on the body as a safety net but fix the root cause.\n- **Touch-friendly spacing**: On mobile, increase padding on list items and nav links. Thumb zone for primary actions is bottom-center of the screen.\n- **Responsive images**: Use `next/image` with `sizes` prop for responsive sizing. For art direction changes, use `<picture>` with `<source media='...'>`. Always set `width` and `height` to prevent layout shift.\n\n**Cognitive load**: Simpler apps feel better, even with the same features.\n- **Max 4 items per group**: Humans hold 4 items in working memory. Navigation menus: max 5 top-level items. Forms: max 4 fields per section. Dashboards: max 4 metrics visible without scrolling. Pricing: max 3 tiers.\n- **Single focus per view**: Every page has ONE primary task. If there are two equal CTAs, neither is primary. One primary action, everything else is secondary or tertiary.\n- **Sequential decisions**: Don't present all options at once. Multi-step forms beat one long form. Each step should have one decision.\n- **Progressive disclosure**: Show the essential first, reveal complexity on demand. Use expandable sections, tabs, or drill-down navigation. The default view should cover 80% of use cases.\n- **Visual grouping**: Related items must be visually grouped (proximity + shared background or border). Unrelated items must have clear separation. The Gestalt principle of proximity is the easiest way to reduce cognitive load.\n- **Reduce choice anxiety**: For important decisions, provide a recommended/default option. Highlight it visually. 'Most popular' badges on pricing plans reduce decision time.\n- **Keep context visible**: In multi-step flows, show a progress indicator and allow going back. Never make users remember what they entered on a previous screen.\n\n**Production hardening**: Designs that only work with perfect data aren't production-ready.\n- **Text overflow**: Use `text-overflow: ellipsis` + `overflow: hidden` for single-line text in constrained spaces. Use `-webkit-line-clamp` for multi-line. Add `min-width: 0` on flex items to prevent overflow.\n- **Extreme inputs**: Test with 100+ character names, emoji in text fields, and empty strings. The layout should not break.\n- **Error resilience**: Every API call needs error UI. Network failures, 404s, 500s all need clear messages with recovery options. Never show raw stack traces.\n\n**Landing page component patterns** (use these for professional, non-generic landing pages):\nBuild these components inline (shadcn + motion + Tailwind is enough):\n- **Animated number counters**: Use `@number-flow/react` (installed) for stat sections. `<NumberFlow value={count} />` with smooth digit transitions.\n- **Marquee / logo ticker**: CSS-only infinite scroll (`@keyframes marquee { from { transform: translateX(0) } to { transform: translateX(-50%) } }`) with duplicated content. Pause on hover.\n- **Bento grid**: CSS grid with varied `col-span` and `row-span`. Each cell gets a unique visual. Never make all cells the same size.\n- **Animated beam / connection lines**: SVG paths with `motion` stroke-dashoffset animation for 'how it works' sections.\n- **Shimmer borders**: `background: conic-gradient(...)` wrapper with animation for hero CTAs.\n- **Comparison tables**: Sticky-header table with check/x icons and row highlighting on the recommended plan.\n- **Testimonial carousel**: CSS scroll-snap for horizontal cards with avatar, quote, name, role. Auto-scroll paused on hover.\n- **Gradient mesh backgrounds**: Multiple layered `radial-gradient` backgrounds for hero sections.\nPick 3-5 per landing page based on the app's content. The goal: every landing page should look like it was designed by a human, not generated by AI.\n\n**Landing page navbar (non-negotiable):** Fully transparent (no background, no border, no backdrop-blur) and NOT sticky/fixed. It sits inside the hero section and scrolls naturally. White text on dark hero backgrounds. Primary CTA button in the nav uses the accent color (solid fill, rounded-md). Do NOT use `sticky`, `fixed`, `top-0`, or `backdrop-blur` on landing page navbars. Dashboard sidebar/nav IS fixed (different pattern).\n\n**Design for THIS app, not any app** (the #1 way to avoid generic AI output):\n- Every layout decision should be informed by what THIS app does. A gym check-in app needs a prominent search bar. A habit tracker needs a daily view. A CRM needs a pipeline. Don't default to the same dashboard-with-stats layout for everything.\n- Write copy that is SPECIFIC to the app's domain. Not 'Manage your data efficiently' but 'Check in members in under 3 seconds'. Every headline, empty state, and CTA should reference what the user actually does in this app.\n- Choose visual emphasis based on the primary action. If the user comes to log a workout, the workout form should be the biggest thing on the dashboard.\n- Use domain-appropriate metaphors. A fitness app uses progress rings. A project tracker uses kanban columns. A recipe app uses cards with photos. Don't use the same card grid for everything.\n\n**NEVER do these (AI slop anti-patterns)**:\nThe following are telltale signs of AI-generated design. Avoid all of them.\n*Visual patterns*:\n- Cyan-on-dark, purple-to-blue gradients, neon accents on dark backgrounds (the AI color palette)\n- Gradient text on headings or metrics (decorative noise, not meaningful)\n- Glassmorphism / blur effects used decoratively (only use blur for overlays/modals that need it)\n- Rounded rectangles with thick colored border on one side (lazy accent, never looks intentional)\n- Dark mode with glowing colored box-shadows on accent elements\n- Small rounded-square icon tiles stacked above headings (the icon-tile pattern)\n- Pure black (#000) backgrounds in dark mode (use tinted dark gray instead)\n- 3-column icon + title + paragraph feature grid (use asymmetric layouts, bento grids, or varied card sizes)\n- Nested cards inside cards (visual noise, flatten the hierarchy)\n- Everything center-aligned (use left-alignment for body content, reserve center for heroes/CTAs)\n- Monotonous spacing (same gap everywhere signals no designer touched this)\n*Copy patterns*:\n- Hero text like 'Transform your X' / 'One Day at a Time' / 'Join thousands' / 'Unlock the power of' (write SPECIFIC copy)\n- Generic feature descriptions ('Powerful analytics', 'Seamless integration', 'Lightning fast')\n- The hero metric template (big number, small label, supporting stats, gradient accent)\n- Repeating information users can already see. Every word must earn its place.\n*Functional patterns*:\n- Bounce/elastic easing on animations (feels dated, real objects decelerate smoothly, use Quart/Expo out)\n- Modals for things that could be inline (modals are a last resort, not a default)\n- Nav links to pages that don't exist. EVERY link in the sidebar/topnav MUST have a corresponding page.\n- FAKE FORMS. NEVER use `setTimeout` or `await new Promise` to simulate API calls. Every form MUST have a real server action (actions.ts with 'use server') that writes to the database. If a form doesn't persist data, the feature is BROKEN.\n\n**Favicon**: A default SVG favicon exists at app/icon.svg. On the landing page step, update it to match the app. Keep it simple (a single icon/letter on a rounded square, using the accent color).\n";var Jn=`# Consumer Warm Archetype
2458
+ ${D}${Q}${Se}${ue}`);let F=[],R=(w,P)=>{F.push({phase:w,message:P})},z=(w,P)=>{let q=F.find(K=>K.phase===w&&!K.durationMs);q&&(q.durationMs=P)};if(e){let w=eo(e.server,e.progressToken,()=>F[F.length-1]?.message??"Setting up project...");e.cleanup=()=>w.stop()}let ge=b?.requestedSubdomain||void 0,G,ke;R("register","Registering project on Mistflow...");let Fe=Date.now();try{let w=await xt(r,void 0,"neon",ge);G=w.id;let P=ve(s,"mistflow.json"),q=JSON.parse(so(P,"utf-8"));if(q.projectId=G,gr(P,JSON.stringify(q,null,2)),St(s,Tt(G,r)),w.managed_env&&Object.keys(w.managed_env).length>0){let K=ve(s,".env.local"),fe=Ve(K)?so(K,"utf-8"):"";for(let[ee,u]of Object.entries(w.managed_env)){let g=new RegExp(`^${ee}=.*$`,"m");g.test(fe)?fe=fe.replace(g,`${ee}=${u}`):fe+=`
2459
+ ${ee}=${u}`}gr(K,fe)}try{let{getBaseUrl:K,getAuthHeaders:fe}=await Promise.resolve().then(()=>(pe(),Kr)),ee=fe(),u=a?.features,g=a?.steps,k={};Array.isArray(u)&&u.length>0&&(k.features=u.map(I=>I.name)),a&&(k.plan=a),Array.isArray(g)&&g.length>0&&(k.provenance=g.map(I=>({feature:I.name??I.title??`Step ${I.number??"?"}`,user_intent:(I.description??"").slice(0,500),decisions:"Seeded from plan at init",tradeoffs:"",files_affected:[]}))),Object.keys(k).length>0&&await fetch(`${K()}/api/projects/${encodeURIComponent(G)}/state`,{method:"PUT",headers:{...ee,"Content-Type":"application/json"},body:JSON.stringify(k)})}catch{}F[F.length-1].message=`Registered as ${G.slice(0,8)}`}catch(w){let P=w instanceof Error?w.message:String(w);console.error("Could not register project on backend:",P),ke=`Project created locally but NOT registered on Mistflow servers (${P}). Deploy will auto-register it.`,F[F.length-1].message="Registration skipped (offline \u2014 deploy will retry)"}z("register",Date.now()-Fe),R("git","Initializing git repository...");let qe=Date.now();try{let w=tl(s);await w.init(),await w.add("."),await w.commit("Initial Mistflow project setup"),F[F.length-1].message="Git repository initialized"}catch{console.error("Git initialization failed, continuing without git."),F[F.length-1].message="Git init skipped"}z("git",Date.now()-qe);let Be=F.reduce((w,P)=>w+(P.durationMs??0),0),me={projectPath:s,projectId:G,status:"awaiting_install"},_e=F.map(w=>{let P=w.durationMs?` (${(w.durationMs/1e3).toFixed(1)}s)`:"";return`${w.message}${P}`});me.progress=_e,me.totalSetupTime=`${(Be/1e3).toFixed(1)}s`;let Re=[];G||Re.push("Project was not registered with Mistflow (not signed in). Run mist_setup to sign in BEFORE deploying \u2014 deploy will fail without it."),ke&&(me.registrationWarning=ke),Re.length>0&&(me.warnings=Re);let Ne=`NEXT: Call mist_install({ projectPath: "${s}" }). This is fire-and-poll \u2014 the first call returns a jobId and status: "running"; re-call with the same jobId every ~15-30s until status is "complete". Typical duration 30-90s. Do NOT ask the user for permission \u2014 install is a required follow-up to init, not a decision point. After install completes, call mist_implement({ projectPath: "${s}" }) to build the first plan step.`;return me.nextAction=G?Ne:`${Ne} IMPORTANT: You MUST also run mist_setup to sign in before deploying \u2014 the project could not be registered because auth is missing.`,c(JSON.stringify(me))}var Gn={name:"mist_init",description:"Initialize a new Next.js project using the Mistflow stack. Downloads templates from the Mistflow registry, installs dependencies, sets up auth, and initializes git. Use this after mist_plan to create the project.",inputSchema:rl,handler:Tl};import{z as yr}from"zod";import{existsSync as Nt,readFileSync as wr,writeFileSync as br,mkdirSync as Hl}from"fs";import{join as dt,resolve as Wl,dirname as Gl}from"path";pe();import{createConnection as Vl}from"net";var Vn="\nApply these choices to every file you create. Customize the shadcn CSS variables in globals.css to match. Do NOT use default shadcn blue/zinc theme.\n\n**CRITICAL: shadcn/ui components are already installed. NEVER write components/ui/*.tsx files from scratch.** If you need a shadcn component, run `npx shadcn@latest add <component>` to pull it. The project already includes: button, card, input, label, form, dialog, table, dropdown-menu, badge, separator, skeleton, sheet, tabs, avatar, select, textarea, checkbox, switch, tooltip, popover, sonner.\n\n**shadcn MCP server is available.** You have the `shadcn` MCP server installed \u2014 use it to browse and search for components, blocks, and landing page sections from the shadcn registry. Before building a landing page section from scratch, check the registry for existing blocks (hero sections, feature grids, pricing tables, testimonials, footers) and customize them instead of reinventing.\n\n**Project routes (use these exact paths):** Login is at `/login` (NOT /sign-in). Register is at `/register` (NOT /sign-up). All landing page and nav links MUST use `/login` and `/register`.\n\n### Design quality rules (non-negotiable):\n\n**Typography**: Use the plan fonts everywhere. font-heading for headings, font-body for body text.\n- **Modular scale**: Use a 1.25-1.5 ratio between sizes. A 5-level system covers most needs: xs (0.75rem captions), sm (0.875rem metadata), base (1rem body), lg (1.25-1.5rem subheadings), xl+ (2-4rem headlines). Sizes too close together (14/15/16px) create muddy hierarchy.\n- **Vertical rhythm**: Your line-height IS your spacing unit. If body is 16px with line-height 1.5 (= 24px), vertical spacing should be multiples of 24px.\n- **Weight contrast**: Use font-medium/font-semibold contrast, not just size. Bold headings + regular body creates natural hierarchy without extra sizes.\n- **Measure**: Set `max-width: 65ch` on text containers. Long lines kill readability.\n- **OpenType polish**: Use `font-variant-numeric: tabular-nums` on data tables for aligned columns.\n- Never use more than 2 font families. One well-chosen family in multiple weights is cleaner than two competing typefaces.\n- Never fall back to system fonts. Never use decorative fonts for body text. Minimum 16px (1rem) for body.\n\n**Color**: Build a functional palette, not a rainbow.\n- **Tinted neutrals**: Pure gray is dead. Add a tiny tint toward the accent color to all your grays (borders, backgrounds, muted text). The tint should be subtle enough not to read as 'colored' but creates subconscious cohesion.\n- **60/30/10 rule**: 60% neutral backgrounds/whitespace, 30% secondary colors (text, borders, inactive states), 10% accent (CTAs, highlights, focus). Accent colors work BECAUSE they're rare. Overuse kills their power.\n- **Dangerous combos to avoid**: Light gray text on white (#1 accessibility fail). Gray text on any colored background (looks washed out and dead, use a darker shade of the background color instead). Yellow text on white. Thin light text on images.\n- **Dark mode is not inverted light mode**: Use lighter surfaces for depth (no shadows). Desaturate accents slightly. Never pure black backgrounds, use dark gray (12-18% lightness). Reduce body text weight slightly (350 vs 400) because light-on-dark reads heavier.\n- Never use pure #000 or #fff. Customize the shadcn CSS variables in globals.css to match the plan palette.\n\n**Layout & space**: Space is a design material, not leftover.\n- **4px base unit**: All spacing should be multiples of 4: 4, 8, 12, 16, 24, 32, 48, 64, 96px. No arbitrary values. Name tokens semantically (`space-sm`, `space-lg`), not by value.\n- **Use `gap` not margins**: Use `gap` for sibling spacing. It eliminates margin collapse hacks and is more predictable. Reserve margin for positioning, not spacing between siblings.\n- **Rhythm through contrast**: Tight groupings within related items (8-12px), generous separation between sections (48-96px). Vary spacing WITHIN sections too. Not every row needs the same gap. The ratio between inner and outer spacing creates hierarchy.\n- **Density matches content**: Data-dense UIs (dashboards, tables, admin panels) need tighter spacing with more information visible. Marketing pages need generous whitespace and breathing room. Match density to what the user is doing.\n- **The squint test**: Blur your eyes. Can you still identify primary, secondary, and clear groupings? If everything looks the same weight, hierarchy is broken. The most important content should be obvious within 2 seconds.\n- **Asymmetry > centering**: Left-aligned with asymmetric layouts feels more designed. Center-alignment is fine for hero sections and CTAs, not for body content.\n- **Flex vs Grid**: Use Flexbox for 1D layouts (rows of items, navbars, button groups, card contents, most component internals). Use Grid for 2D layouts (page-level structure, dashboards, data-dense interfaces where rows AND columns need coordinated control). Don't default to Grid when Flexbox with `flex-wrap` would be simpler. Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Use named `grid-template-areas` for complex page layouts and redefine at breakpoints.\n- **Cards earn their existence**: Only use cards when content needs clear separation, grid comparison, or interaction boundaries. Not everything needs a container. Never nest cards inside cards. Vary card sizes or mix cards with non-card content to break repetition.\n- **Section variety**: Don't make every section the same structure. Alternate layouts (text-left/image-right, then image-left/text-right), vary section heights, mix full-width with contained content. Monotonous section rhythm signals no designer touched this.\n- **Z-index scale**: Use a semantic scale, not arbitrary numbers. dropdown(10) -> sticky(20) -> modal-backdrop(30) -> modal(40) -> toast(50) -> tooltip(60). Never use 999 or 9999.\n- **Touch targets**: Minimum 44x44px for all interactive elements, even if the visual element is smaller (use padding).\n\n**Cards & surfaces**: Match the plan's cardStyle. Stat cards need background fills with the accent color as a subtle icon background or tinted left border. Use bg-muted/30 for section differentiation between page areas. Build a consistent shadow scale (sm -> md -> lg) and use elevation to reinforce hierarchy, not as decoration.\n\n**Sidebar**: App name + icon at top. Nav items with hover:bg-muted rounded-md transition. Active item: bg-primary/10 text-primary font-medium. User email + sign out at bottom. The sidebar should feel like a distinct surface (bg-card or bg-muted/50).\n**Mobile navigation**: The desktop sidebar must collapse to a hamburger menu on mobile. Use the shadcn `Sheet` component: a hamburger icon button (visible at `md:hidden`) opens a Sheet from the left containing the same nav items. The desktop sidebar is `hidden md:flex`. For simple apps with 3-5 nav items, a bottom tab bar (`fixed bottom-0`) is an alternative. Always respect `env(safe-area-inset-bottom)` for notched phones.\n\n**Interaction design**: Every interactive element needs 8 states, not just default and hover.\n- **Required states**: default, hover, focus, active/pressed, disabled, loading, error, success. Design all of them intentionally.\n- **Focus rings**: Use `:focus-visible` (not `:focus`) so rings show for keyboard users but not mouse clicks. Never remove focus indicators without replacement.\n- **Button hierarchy**: Don't make every button primary. Use ghost, outline, secondary, and text-link variants. One primary CTA per view.\n- **Progressive disclosure**: Simple first, advanced behind expandable sections. Don't overwhelm with options.\n- **Undo over confirmation**: For destructive actions, remove immediately + show an undo toast, then permanently delete after timeout. Better than confirmation dialogs that users click through blindly.\n- **Form patterns**: Visible `<label>` elements always (placeholders aren't labels, they disappear). Validate on blur, not on keystroke. Show format expectations with examples.\n\n**Empty states**: Every empty list/table needs a rich empty state. Not just 'Nothing here'. Include: a simple inline SVG illustration (48x48, relevant to the feature), a specific message that teaches ('Create your first habit to start tracking your daily routine'), and a primary action button. Empty states are the first thing new users see. Make them inviting.\n\n**Loading states**: Use the shadcn `Skeleton` component. Show skeleton versions of cards, lists, and stats during data fetches. Example: `<Skeleton className=\"h-8 w-32\" />` for a stat number, `<Skeleton className=\"h-20 w-full\" />` for a card. Add a `loading.tsx` file in dashboard route groups. Never show a blank page or a lone spinner.\n\n**Images**: For Unsplash images on landing pages, use `next/image` with `placeholder=\"blur\"` and a blurDataURL (a tiny 10x6 base64 image \u2014 generate a solid color blur like `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAGCAYAAAD68A/GAAAAP0lEQVQYV2N89+7dfwYGBgZGRkYGBgYmBjIBE7kaPHv27D8DA8N/BgYGRkZGRgYGJgYyARO5GkjWQHEoxRoAAPyTGAGBMpEAAAAASUVORK5CYII=`). This creates a smooth shimmer-in effect instead of images popping in.\n\n**Motion** (`motion` package is installed):\n\n**Hero moment strategy**: Pick ONE signature animation per page. Not scattered animations on everything. On a landing page, the hero entrance is your hero moment. On a dashboard, a satisfying success animation after the primary action. One great animation > ten mediocre ones. Everything else gets subtle transitions.\n\n**Animation opportunity audit** -- check these 5 categories:\n1. **Missing feedback**: Button has no press response? Add `active:scale-[0.97]` (CSS) or `whileTap={{ scale: 0.97 }}` (motion). Toggle has no slide? Animate it.\n2. **Jarring transitions**: Menu appears instantly? Add 200ms fade + slide. Content swaps without transition? Crossfade between states.\n3. **Unclear relationships**: Parent-child not visually connected? Stagger children 50ms after parent. Deleted item just disappears? Animate it out (opacity + height collapse).\n4. **Lack of delight**: Success state is flat? Draw a checkmark SVG with stroke-dashoffset animation. Empty state is static? Add a gentle float on the illustration.\n5. **Missed guidance**: First-time user gets no hint? Pulse the primary CTA once on page load. New feature unnoticed? Brief highlight animation.\n\n**Timing hierarchy**: 100-150ms for instant feedback (buttons, toggles). 200-300ms for state changes (menus, tooltips). 300-500ms for layout shifts (accordions, modals). 500-800ms for entrances (page loads). Exit animations run at 75% of entrance duration.\n\n**Easing**: Use exponential curves (Quart out, Expo out) for sophisticated physics. Never `ease` (generic), never bounce/elastic (dated). Real objects decelerate smoothly. CSS: `cubic-bezier(0.25, 1, 0.5, 1)` (Quart out) or `cubic-bezier(0.16, 1, 0.3, 1)` (Expo out).\n\n**Micro-interaction recipes**:\n- **Button press**: `active:scale-[0.97] transition-transform duration-100` (CSS only, no motion needed)\n- **Success checkmark**: SVG `<path>` with `stroke-dasharray` + `stroke-dashoffset` animation from full to 0. 400ms Expo out.\n- **Height expand/collapse**: `grid-template-rows: 0fr -> 1fr` with `transition: grid-template-rows 300ms`. No JS layout measurement needed.\n- **Skeleton to content**: Skeleton pulses (CSS `animate-pulse`), then crossfades to real content with `opacity` transition. Use the pre-scaffolded `<ContentLoader>` component.\n- **Staggered list entrance**: CSS `animation-delay` on children: `.animate-fade-up:nth-child(1) { animation-delay: 0ms } :nth-child(2) { animation-delay: 50ms }` etc. Or use `<StaggerList>` component.\n- **Number count-up**: Use `@number-flow/react` for smooth digit morphing on stats/metrics.\n\n**Performance**: Only animate `transform` and `opacity` (GPU-accelerated). Never animate width, height, top, left, margin, or padding. For scroll-tied effects on landing pages, use CSS `animation-timeline: scroll()` where supported (progressive enhancement).\n\n**Accessibility**: Always respect `prefers-reduced-motion`. Wrap animations in `@media (prefers-reduced-motion: no-preference) { ... }`. Provide static alternatives that convey the same information.\n\n**SSR-safe (CRITICAL)**: This app deploys to Mistflow Cloud's edge runtime where IntersectionObserver may not fire. NEVER use `initial={{ opacity: 0 }}` (content stays invisible forever). Use CSS @keyframes for entrance effects:\n```tsx\n// SAFE: CSS @keyframes in globals.css:\n// @keyframes fade-up { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }\n// .animate-fade-up { animation: fade-up 0.5s ease-out both; }\n// Use motion ONLY for hover/tap/gesture interactions:\nimport { motion } from 'motion/react';\n<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.97 }} transition={{ duration: 0.15 }}>\n// NEVER: <motion.div initial={{ opacity: 0 }} whileInView={{ opacity: 1 }}>\n```\nFor dashboard pages, keep animations minimal. Button press feedback + skeleton loading + success states. No entrance animations on dashboard pages.\n\n**UX writing**: Every word earns its place.\n- **Buttons**: Use outcome-focused verb + object ('Save changes', 'Create account'), never generic ('Submit', 'OK', 'Click here').\n- **Destructive actions**: State exact consequences ('Delete 5 items permanently', not 'Delete selected'). Distinguish 'Delete' (permanent) from 'Remove' (recoverable).\n- **Error messages**: Answer three questions: what happened, why, and how to fix it. 'Email address is not valid. Include an @ symbol.' not 'Invalid input'. Never blame the user.\n- **Empty states**: Explain what will appear here, why it matters, and give a clear next action.\n- **Loading/progress**: Set time expectations. 'Deploying your app (usually takes 30 seconds)' not just a spinner.\n- **Consistency**: Pick one term and stick with it. 'Delete' everywhere, not Delete/Remove/Trash interchangeably.\n- **Link text**: Make it standalone for accessibility. 'View pricing plans' not 'Click here'.\n\n**Responsive design**: Mobile-first, then enhance.\n- **Mobile-first CSS**: Write base styles for mobile, then use `min-width` media queries to enhance for larger screens. Never start with desktop and try to cram it into mobile.\n- **Input method matters more than screen size**: Use `@media (pointer: fine)` for mouse/trackpad (smaller targets OK, hover effects work), `@media (pointer: coarse)` for touch (44px targets required, no hover-dependent UI). Use `@media (hover: none)` to detect devices that can't hover. Never require hover to reveal functionality.\n- **Safe areas**: Add `<meta name='viewport' content='width=device-width, initial-scale=1, viewport-fit=cover'>` and use `env(safe-area-inset-bottom)` for bottom navigation on notched phones. Fixed bottom bars must respect safe areas.\n- **Self-adjusting grids**: Use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for card grids. Columns reflow naturally without breakpoint management.\n- **Container queries over media queries**: For reusable components, use `container-type: inline-size` on the parent and `@container (min-width: 400px)` on children. Components adapt to their container, not the viewport.\n- **No horizontal scroll**: Nothing should overflow the viewport width. Test at 320px width (smallest common phone). Use `overflow-x: hidden` on the body as a safety net but fix the root cause.\n- **Touch-friendly spacing**: On mobile, increase padding on list items and nav links. Thumb zone for primary actions is bottom-center of the screen.\n- **Responsive images**: Use `next/image` with `sizes` prop for responsive sizing. For art direction changes, use `<picture>` with `<source media='...'>`. Always set `width` and `height` to prevent layout shift.\n\n**Cognitive load**: Simpler apps feel better, even with the same features.\n- **Max 4 items per group**: Humans hold 4 items in working memory. Navigation menus: max 5 top-level items. Forms: max 4 fields per section. Dashboards: max 4 metrics visible without scrolling. Pricing: max 3 tiers.\n- **Single focus per view**: Every page has ONE primary task. If there are two equal CTAs, neither is primary. One primary action, everything else is secondary or tertiary.\n- **Sequential decisions**: Don't present all options at once. Multi-step forms beat one long form. Each step should have one decision.\n- **Progressive disclosure**: Show the essential first, reveal complexity on demand. Use expandable sections, tabs, or drill-down navigation. The default view should cover 80% of use cases.\n- **Visual grouping**: Related items must be visually grouped (proximity + shared background or border). Unrelated items must have clear separation. The Gestalt principle of proximity is the easiest way to reduce cognitive load.\n- **Reduce choice anxiety**: For important decisions, provide a recommended/default option. Highlight it visually. 'Most popular' badges on pricing plans reduce decision time.\n- **Keep context visible**: In multi-step flows, show a progress indicator and allow going back. Never make users remember what they entered on a previous screen.\n\n**Production hardening**: Designs that only work with perfect data aren't production-ready.\n- **Text overflow**: Use `text-overflow: ellipsis` + `overflow: hidden` for single-line text in constrained spaces. Use `-webkit-line-clamp` for multi-line. Add `min-width: 0` on flex items to prevent overflow.\n- **Extreme inputs**: Test with 100+ character names, emoji in text fields, and empty strings. The layout should not break.\n- **Error resilience**: Every API call needs error UI. Network failures, 404s, 500s all need clear messages with recovery options. Never show raw stack traces.\n\n**Landing page component patterns** (use these for professional, non-generic landing pages):\nBuild these components inline (shadcn + motion + Tailwind is enough):\n- **Animated number counters**: Use `@number-flow/react` (installed) for stat sections. `<NumberFlow value={count} />` with smooth digit transitions.\n- **Marquee / logo ticker**: CSS-only infinite scroll (`@keyframes marquee { from { transform: translateX(0) } to { transform: translateX(-50%) } }`) with duplicated content. Pause on hover.\n- **Bento grid**: CSS grid with varied `col-span` and `row-span`. Each cell gets a unique visual. Never make all cells the same size.\n- **Animated beam / connection lines**: SVG paths with `motion` stroke-dashoffset animation for 'how it works' sections.\n- **Shimmer borders**: `background: conic-gradient(...)` wrapper with animation for hero CTAs.\n- **Comparison tables**: Sticky-header table with check/x icons and row highlighting on the recommended plan.\n- **Testimonial carousel**: CSS scroll-snap for horizontal cards with avatar, quote, name, role. Auto-scroll paused on hover.\n- **Gradient mesh backgrounds**: Multiple layered `radial-gradient` backgrounds for hero sections.\nPick 3-5 per landing page based on the app's content. The goal: every landing page should look like it was designed by a human, not generated by AI.\n\n**Landing page navbar (non-negotiable):** Fully transparent (no background, no border, no backdrop-blur) and NOT sticky/fixed. It sits inside the hero section and scrolls naturally. White text on dark hero backgrounds. Primary CTA button in the nav uses the accent color (solid fill, rounded-md). Do NOT use `sticky`, `fixed`, `top-0`, or `backdrop-blur` on landing page navbars. Dashboard sidebar/nav IS fixed (different pattern).\n\n**Design for THIS app, not any app** (the #1 way to avoid generic AI output):\n- Every layout decision should be informed by what THIS app does. A gym check-in app needs a prominent search bar. A habit tracker needs a daily view. A CRM needs a pipeline. Don't default to the same dashboard-with-stats layout for everything.\n- Write copy that is SPECIFIC to the app's domain. Not 'Manage your data efficiently' but 'Check in members in under 3 seconds'. Every headline, empty state, and CTA should reference what the user actually does in this app.\n- Choose visual emphasis based on the primary action. If the user comes to log a workout, the workout form should be the biggest thing on the dashboard.\n- Use domain-appropriate metaphors. A fitness app uses progress rings. A project tracker uses kanban columns. A recipe app uses cards with photos. Don't use the same card grid for everything.\n\n**NEVER do these (AI slop anti-patterns)**:\nThe following are telltale signs of AI-generated design. Avoid all of them.\n*Visual patterns*:\n- Cyan-on-dark, purple-to-blue gradients, neon accents on dark backgrounds (the AI color palette)\n- Gradient text on headings or metrics (decorative noise, not meaningful)\n- Glassmorphism / blur effects used decoratively (only use blur for overlays/modals that need it)\n- Rounded rectangles with thick colored border on one side (lazy accent, never looks intentional)\n- Dark mode with glowing colored box-shadows on accent elements\n- Small rounded-square icon tiles stacked above headings (the icon-tile pattern)\n- Pure black (#000) backgrounds in dark mode (use tinted dark gray instead)\n- 3-column icon + title + paragraph feature grid (use asymmetric layouts, bento grids, or varied card sizes)\n- Nested cards inside cards (visual noise, flatten the hierarchy)\n- Everything center-aligned (use left-alignment for body content, reserve center for heroes/CTAs)\n- Monotonous spacing (same gap everywhere signals no designer touched this)\n*Copy patterns*:\n- Hero text like 'Transform your X' / 'One Day at a Time' / 'Join thousands' / 'Unlock the power of' (write SPECIFIC copy)\n- Generic feature descriptions ('Powerful analytics', 'Seamless integration', 'Lightning fast')\n- The hero metric template (big number, small label, supporting stats, gradient accent)\n- Repeating information users can already see. Every word must earn its place.\n*Functional patterns*:\n- Bounce/elastic easing on animations (feels dated, real objects decelerate smoothly, use Quart/Expo out)\n- Modals for things that could be inline (modals are a last resort, not a default)\n- Nav links to pages that don't exist. EVERY link in the sidebar/topnav MUST have a corresponding page.\n- FAKE FORMS. NEVER use `setTimeout` or `await new Promise` to simulate API calls. Every form MUST have a real server action (actions.ts with 'use server') that writes to the database. If a form doesn't persist data, the feature is BROKEN.\n\n**Favicon**: A default SVG favicon exists at app/icon.svg. On the landing page step, update it to match the app. Keep it simple (a single icon/letter on a rounded square, using the accent color).\n";var Jn=`# Consumer Warm Archetype
2464
2460
 
2465
2461
  Component-level design guidance for personal, lifestyle, and wellness apps. Habits, journals, recipes, mood trackers, meditation, daily routines, personal finance.
2466
2462
 
@@ -5312,9 +5308,9 @@ A footer with the same five links ("Product / Pricing / Docs / Blog / Terms") is
5312
5308
  `)}async function nc(t){try{let e=await Wo("nextjs",t);return{reminders:e.reminders,skill:e.skill}}catch{return{reminders:`### ${t} step
5313
5309
  - Follow existing patterns in the codebase
5314
5310
  - Server Components by default, "use client" only when interactivity is needed`,skill:""}}}async function sc(t,e,r,o,n,i){let s=[];s.push(`## Step ${t.number}: ${t.name}`),s.push(""),s.push("### What to build:"),s.push(t.description),s.push(""),e.primaryAction&&(s.push("### Primary user action (non-negotiable):"),s.push(`- **Core action**: ${e.primaryAction.action}`),s.push(`- **User flow**: ${e.primaryAction.flow}`),s.push(`- **Dashboard must show**: ${e.primaryAction.dashboardSurface}`),s.push(""),s.push("The dashboard is an ACTION surface, not a stats display. Users must be able to complete the core action directly from the dashboard without navigating to a separate page. If this step builds the dashboard, make sure the primary action is front and center with inline forms/buttons \u2014 not behind a link to another page."),s.push(""));let l=["landing","design","dashboard","crud","layout","admin","general","auth"].includes(n);if(e.design&&l?(s.push(rc(e.design,{hasDesignPreset:!!e.landingDesign&&(n==="landing"||n==="design")})),s.push("")):e.design&&!l&&(s.push("### Design tokens (for reference only \u2014 this step is not UI-focused):"),e.design.fonts&&s.push(`- Fonts: ${e.design.fonts.heading} / ${e.design.fonts.body}`),s.push("- Colors come from CSS variables (`--color-primary`, `--color-background`, etc.) \u2014 not Tailwind palette names."),s.push("")),i){let b=Cn(i);if(b.length>0){s.push("### Approved wireframe (MUST READ before writing any files):"),s.push("The user approved a wireframe sketch before building. **Read these files NOW before writing any code for this step:**");for(let f of b){let S=f.replace(i,"").replace(/^\//,"");s.push(`- \`${S}\``)}s.push(""),s.push("The wireframe defines the LAYOUT and INFORMATION HIERARCHY \u2014 what goes where, what's prominent, what's secondary. It includes HTML comments explaining WHY things are placed where they are."),s.push(""),s.push("The wireframe is intentionally rough (grayscale, system fonts). Your job is to:"),s.push("1. **Keep the same layout structure** \u2014 same information hierarchy, same element placement, same sections in the same order"),s.push("2. **Apply the design tokens** \u2014 colors, fonts, shadows, radius from the plan design choices above"),s.push("3. **Elevate the visual quality** \u2014 make it feel designed for THIS specific app, not generic"),s.push("4. **Respect the HTML comments** \u2014 they explain WHY things are placed where they are"),s.push("5. **If the wireframe shows a search bar at the top of the dashboard, your dashboard MUST have a search bar at the top** \u2014 do not rearrange the layout"),s.push("")}}e.roles&&Array.isArray(e.roles)&&e.roles.length>0&&(s.push("### Role system (from plan):"),s.push(`- Roles: ${e.roles.join(", ")}`),s.push(`- Default role for new signups: ${e.defaultRole??e.roles[0]}`),s.push("- Role helpers are in `lib/roles.ts` \u2014 use `getUserRole()` and `hasRole()` for access checks"),s.push("")),e.multiTenant&&(s.push("### Multi-tenant (from plan):"),s.push("- Organization tables are in `db/schema/organization.ts`"),s.push("- Org helpers are in `lib/org.ts` \u2014 use `getCurrentOrg()` to scope queries"),s.push("- All data queries MUST be scoped to the current org (filter by orgId)"),s.push("- Org switcher component is at `components/org-switcher.tsx`"),s.push("- CRITICAL: `getCurrentOrg()` returns null for new users who haven't created an org yet. The dashboard MUST handle this \u2014 if currentOrg is null, redirect to an onboarding page or show an inline 'Create your first team/workspace' form. NEVER call .id on a null org."),s.push("- WARNING: cookies().set() in server actions does NOT work on Mistflow Cloud's edge runtime. Do NOT use setCurrentOrgId() or any cookies().set() call inside server actions. Instead, pass the orgId as a form field or query param, or store the active org in the user's database record."),s.push("")),e.language&&(s.push(`### Language: ${e.language}`),s.push(`ALL user-facing text must be written in ${e.language}:`),s.push("- Page titles, headings, labels, button text, placeholder text"),s.push("- Navigation items, menu labels, footer text"),s.push("- Error messages, success messages, empty states"),s.push("- Landing page copy, marketing text, CTAs"),s.push("- Form labels and validation messages"),s.push("Code (variable names, comments, file names) stays in English."),s.push(`Set the HTML lang attribute to the appropriate locale code for ${e.language}.`),s.push(""));let d=["landing","design","auth","general","crud","dashboard"];e.audienceType&&d.includes(n)&&(e.audienceType==="b2c"?(s.push("### Audience: this app belongs to ONE business. The landing page talks TO their customers."),s.push("- Hero: what the customer gets ('Exceptional catering for your next event'), NOT what the tool does"),s.push("- CTAs: customer action ('Order Catering', 'Book Now'), NOT business action ('Get Started Free')"),s.push("- Testimonials: from customers ('They catered our wedding'), NOT from business owners"),s.push("- Features: customer benefits ('Specify your dietary needs'), NOT business benefits ('Track preferences')"),s.push("- Stats: social proof for customers ('2,400+ events served'), NOT internal metrics ('$48k revenue')"),s.push("- The business name IS the brand. Say it like a business homepage, not a SaaS onboarding."),s.push("")):e.audienceType==="b2b"?(s.push("### Audience: this is a SaaS platform. The landing page pitches TO business owners."),s.push("- Hero: the business pain + solution ('Catering orders managed in one place')"),s.push("- CTAs: business owner action ('Start Free Trial', 'Get Started')"),s.push("- Testimonials: from business owners who use the platform"),s.push("- Features: business benefits ('Track dietary preferences across all orders')"),s.push("- Stats: platform metrics ('500+ businesses', '50K+ orders processed')"),s.push("")):e.audienceType==="internal"&&(s.push("### Audience: internal staff tool. No marketing copy needed."),s.push("- No landing page. Auth page copy is functional: 'Sign in to continue'."),s.push("- Dashboard focuses on operational efficiency, not onboarding or sales."),s.push(""))),r.length>0&&(s.push("### Already completed:"),r.forEach(b=>s.push(`- ${b}`)),s.push(""));let h=e.dataModel?fs(t,e.dataModel):[];h.length>0&&(s.push("### Data model (from plan):"),h.forEach(b=>{let f=io(b),S=Ql(b.fields);s.push(`- **${f}**: ${S}`),s.push(` Schema file: \`db/schema/${f.toLowerCase().replace(/\s+/g,"-")}.ts\``)}),s.push(""));let m=e.pages?Zl(t,e.pages):[];if(m.length>0&&(s.push("### Pages to create/update:"),m.forEach(b=>{let f=b.description?` \u2014 ${b.description}`:"";s.push(`- \`${gs(b)}\`${f}`)}),s.push("")),n==="crud"&&h.length>0&&h.forEach(b=>{let f=io(b),S=f.toLowerCase().replace(/\s+/g,"-"),A=S.endsWith("s")?S:`${S}s`;s.push(`### Files for ${f} CRUD:`),s.push(`- List page: \`app/(dashboard)/${A}/page.tsx\` (Server Component)`),s.push(`- Detail page: \`app/(dashboard)/${A}/[id]/page.tsx\``),s.push(`- Create page: \`app/(dashboard)/${A}/new/page.tsx\``),s.push(`- Server Actions: \`app/(dashboard)/${A}/actions.ts\``),s.push(`- DataTable columns: \`components/${S}-table-columns.tsx\``),s.push(`- Form: \`components/${S}-form.tsx\``),s.push("")}),l){s.push("## Design Doctrine (the standard for every UI step)"),s.push(""),s.push(as),s.push(""),s.push("## Design Reference Library"),s.push(""),s.push("### Typography"),s.push(ls),s.push(""),s.push("### Color"),s.push(cs),s.push(""),s.push("### Motion"),s.push(ds),s.push(""),s.push("### Spatial Composition"),s.push(ps),s.push(""),s.push("### Interaction"),s.push(us),s.push(""),s.push("### UX Writing"),s.push(ms),s.push("");let b=i?dt(i,"DESIGN.md"):void 0,f=b&&Nt(b)?(()=>{try{return wr(b,"utf-8")}catch{return null}})():null;f&&(s.push("### Design system (source of truth: DESIGN.md):"),s.push(""),s.push("The project's DESIGN.md defines the visual identity. Follow it exactly. It's emitted in google-labs-code/design.md format (YAML front matter with colors/typography/rounded/spacing tokens, plus markdown rationale). All UI elements must use the project's CSS custom properties (--color-primary, --color-background, etc. \u2014 these are generated from the YAML tokens) and the fonts configured in layout.tsx. If DESIGN.md and this plan disagree, DESIGN.md wins. The user may have edited it."),s.push(""),s.push(f),s.push(""))}(n==="landing"||n==="design")&&(s.push(is),s.push(""));let p=t.integrationId?st(t.integrationId):void 0;if(p){let b=it(p.id);if(s.push("### Integration blueprint (follow this closely):"),s.push(""),s.push(`Using integration: **${p.name}** (${p.category})`),b?.docsUrl&&s.push(`Official docs: ${b.docsUrl}`),b?.envVars?.length){s.push(""),s.push("**Required environment variables:**");for(let f of b.envVars)s.push(`- \`${f.key}\`: ${f.description} \u2014 Get it at ${f.setupUrl}`);s.push(""),s.push("**IMPORTANT: Never ask the user to paste API keys in chat.** Direct them to set keys in the Mistflow dashboard (Project Settings > Environment Variables) or at app.mistflow.ai. Use mist_config resource='env' action='list' to check if keys are set (has_value: true/false). Only proceed with deploy once all required keys are set.")}b?.packages?.length&&(s.push(""),s.push(`**Packages to install:** \`npm install ${b.packages.join(" ")}\``)),s.push(""),s.push("---"),s.push(p.prompt),s.push("---"),s.push(""),s.push("**Adaptation rules**: Follow the file structure and code patterns above. Replace placeholder values (app names, URLs, copy) with this app's specific content. Use the app's existing data models and route structure. Never ask the user to paste API keys or secrets in the chat. Direct them to set keys in the Mistflow dashboard."),s.push("")}let{reminders:y,skill:x}=await nc(n);return s.push(y),s.push(""),x&&!(n==="landing"&&e.landingDesign==="freeform")&&(s.push(`### ${n} reference:`),s.push(x),s.push("")),l&&(s.push("## Self-Audit \u2014 run before submitting this file"),s.push(""),s.push(`Read the file you just wrote and answer each of these. If any answer is "yes", REDESIGN the failing piece \u2014 don't tweak classes on the same template. A different composition is required.`),s.push(""),s.push('1. **Pill badge at the top?** A small rounded label saying "Built for" / "New:" / "Made for" with a colored dot? \u2192 redesign the hero without it.'),s.push("2. **Fake browser chrome?** A rounded box with red/yellow/green dots and a fake URL bar? \u2192 delete it. Show real components instead, or no preview."),s.push("3. **Centered single-column hero?** Pill \u2192 headline \u2192 subhead \u2192 two CTAs stacked vertically? \u2192 redesign asymmetrically (see spatial.md)."),s.push("4. **Three checkmark benefit bullets** below the hero CTAs? \u2192 remove the triplet pattern."),s.push("5. **Tailwind palette utilities?** Any `bg-emerald-*`, `bg-amber-*`, `text-violet-*`, `border-slate-*`, `from-purple-*`, etc.? \u2192 replace with token classes (`bg-primary`, `bg-success`, `bg-muted`, `border-border`). No exceptions."),s.push("6. **Inter, Geist, Roboto, Arial, or system-ui** as the primary font? \u2192 pick something distinctive (see typography.md)."),s.push("7. **Purple gradient on white background** anywhere? \u2192 that's the single most overused AI-design pattern. Replace."),s.push('8. **Hardcoded hex values** (e.g. `style={{ color: "#10b981" }}`) or named CSS colors (`red`, `lightgrey`) in className or inline styles? \u2192 replace with CSS variables.'),s.push('9. **Generic copy** ("Boost your productivity", "The platform teams love", "Everything you need to")? \u2192 rewrite with specific nouns, numbers, or mechanisms from this product (see writing.md).'),s.push("10. **Zero memorable moments?** Is there ONE signature element \u2014 typographic hero, orchestrated reveal, asymmetric break, product-specific illustration? If zero, the page is generic. Add one (see motion.md, typography.md)."),s.push(""),s.push(`If every answer is "no, I'm good" \u2014 then submit.`),s.push("")),s.join(`
5315
- `)}async function ic(t){let{projectPath:e,step:r}=t,o=Wl(e??process.cwd()),n=Yl(o);if(!n)return Ie(o);if(!Nt(dt(o,"node_modules")))return c(`Dependencies are not installed at ${o}. Call mist_install and projectPath='${o}' before running implement. This is a one-time setup step after init.`,!0);try{let{ensureBackendRegistered:v,ensureShadcnComponents:M}=await Promise.resolve().then(()=>(Zo(),Xo));await v(o);let H=await M(o);H.failed?console.error(`[implement] ${H.failed}`):H.installed.length>0&&console.error(`[implement] installed ${H.installed.length} shadcn components`)}catch(v){console.error("[implement] self-heal skipped:",v instanceof Error?v.message:String(v))}let i=n.plan;if(!i||!i.steps||i.steps.length===0)return c("No project plan found. Start by describing your app idea first \u2014 the AI will create a plan for you.",!0);let s,a=i.steps.find(v=>v.status==="in_progress");if(a){let v=i.steps.findIndex(M=>M.number===a.number);v!==-1&&(i.steps[v].status="completed",s=`Auto-completed step ${a.number} (${a.name})`,hs(o,n))}let l;if(r!==void 0){if(l=i.steps.find(v=>v.number===r),!l)return c(`Step ${r} not found. The plan has ${i.steps.length} steps (numbered ${i.steps[0].number} to ${i.steps[i.steps.length-1].number}).`,!0)}else if(l=i.steps.find(v=>v.status!=="completed"),!l)return c(JSON.stringify({message:"All plan steps are completed!",completedSteps:i.steps.map(v=>v.name),nextAction:"NEXT: Deploy the app now. Call mist_deploy action='deploy'. Do NOT suggest localhost or ask the user \u2014 just deploy."}));let d=i.steps.filter(v=>v.status==="completed").map(v=>`Step ${v.number}: ${v.name}`),{readLocalState:h}=await Promise.resolve().then(()=>(We(),Pt)),m=h(o),p=tc(l),y=[];if((p==="crud"||p==="schema")&&i.dataModel&&l.entities&&l.entities.length>0){let v=fs(l,i.dataModel);for(let M of v){let H=io(M);try{let W=(M.fields||[]).map(D=>typeof D=="string"?{name:D,type:"text"}:{name:D.name,type:Xl(D.type),required:D.required!==!1});if(W.length===0)continue;let oe=n.dbProvider==="neon"?"nextjs-neon":"nextjs",N=await Go(oe,H,W),_=0,re=0;for(let D of N.files){let Q=dt(o,D.path);if(Nt(Q)){re++;continue}Hl(Gl(Q),{recursive:!0}),br(Q,D.content),_++}let ce=dt(o,"db","index.ts");if(Nt(ce)){let D=wr(ce,"utf-8");D.includes(N.dbExport)||br(ce,D.trimEnd()+`
5311
+ `)}async function ic(t){let{projectPath:e,step:r}=t,o=Wl(e??process.cwd()),n=Yl(o);if(!n)return Ie(o);if(!Nt(dt(o,"node_modules")))return c(`Dependencies are not installed at ${o}. Call mist_install and projectPath='${o}' before running implement. This is a one-time setup step after init.`,!0);try{let{ensureBackendRegistered:v,ensureShadcnComponents:M}=await Promise.resolve().then(()=>(Zo(),Xo));await v(o);let H=await M(o);H.failed?console.error(`[implement] ${H.failed}`):H.installed.length>0&&console.error(`[implement] installed ${H.installed.length} shadcn components`)}catch(v){console.error("[implement] self-heal skipped:",v instanceof Error?v.message:String(v))}let i=n.plan;if(!i||!i.steps||i.steps.length===0)return c("No project plan found. Start by describing your app idea first \u2014 the AI will create a plan for you.",!0);let s,a=i.steps.find(v=>v.status==="in_progress");if(a){let v=i.steps.findIndex(M=>M.number===a.number);v!==-1&&(i.steps[v].status="completed",s=`Auto-completed step ${a.number} (${a.name})`,hs(o,n))}let l;if(r!==void 0){if(l=i.steps.find(v=>v.number===r),!l)return c(`Step ${r} not found. The plan has ${i.steps.length} steps (numbered ${i.steps[0].number} to ${i.steps[i.steps.length-1].number}).`,!0)}else if(l=i.steps.find(v=>v.status!=="completed"),!l)return c(JSON.stringify({message:"All plan steps are completed!",completedSteps:i.steps.map(v=>v.name),nextAction:"NEXT: Deploy the app now. Call mist_deploy action='deploy'. Do NOT suggest localhost or ask the user \u2014 just deploy."}));let d=i.steps.filter(v=>v.status==="completed").map(v=>`Step ${v.number}: ${v.name}`),{readLocalState:h}=await Promise.resolve().then(()=>(We(),Pt)),m=h(o),p=tc(l),y=[];if((p==="crud"||p==="schema")&&i.dataModel&&l.entities&&l.entities.length>0){let v=fs(l,i.dataModel);for(let M of v){let H=io(M);try{let W=(M.fields||[]).map(D=>typeof D=="string"?{name:D,type:"text"}:{name:D.name,type:Xl(D.type),required:D.required!==!1});if(W.length===0)continue;let re=n.dbProvider==="neon"?"nextjs-neon":"nextjs",N=await Go(re,H,W),_=0,ne=0;for(let D of N.files){let Q=dt(o,D.path);if(Nt(Q)){ne++;continue}Hl(Gl(Q),{recursive:!0}),br(Q,D.content),_++}let ce=dt(o,"db","index.ts");if(Nt(ce)){let D=wr(ce,"utf-8");D.includes(N.dbExport)||br(ce,D.trimEnd()+`
5316
5312
  `+N.dbExport+`
5317
- `)}_>0?y.push(`${N.entityPascal} CRUD (${_} new files${re>0?`, ${re} existing skipped`:""})`):re>0&&y.push(`${N.entityPascal} CRUD (all ${re} files already exist \u2014 skipped)`)}catch(W){console.error(`Module generation failed for ${H} (non-fatal):`,W instanceof Error?W.message:W)}}}let x=await sc(l,i,d,null,p,o),b=i.steps.findIndex(v=>v.number===l.number);if(b!==-1&&(n.plan.steps[b].status="in_progress",hs(o,n)),m&&n.projectId){let{syncRemoteState:v}=await Promise.resolve().then(()=>(We(),Pt));v(n.projectId,m).catch(()=>{})}let f=i.steps.every(v=>v.status==="completed"||v.number===l.number),S;f?S=`THIS IS THE LAST STEP. Rules for speed:
5313
+ `)}_>0?y.push(`${N.entityPascal} CRUD (${_} new files${ne>0?`, ${ne} existing skipped`:""})`):ne>0&&y.push(`${N.entityPascal} CRUD (all ${ne} files already exist \u2014 skipped)`)}catch(W){console.error(`Module generation failed for ${H} (non-fatal):`,W instanceof Error?W.message:W)}}}let x=await sc(l,i,d,null,p,o),b=i.steps.findIndex(v=>v.number===l.number);if(b!==-1&&(n.plan.steps[b].status="in_progress",hs(o,n)),m&&n.projectId){let{syncRemoteState:v}=await Promise.resolve().then(()=>(We(),Pt));v(n.projectId,m).catch(()=>{})}let f=i.steps.every(v=>v.status==="completed"||v.number===l.number),S;f?S=`THIS IS THE LAST STEP. Rules for speed:
5318
5314
 
5319
5315
  1. Write ALL files using PARALLEL tool calls \u2014 batch multiple Write/Edit calls in a single message.
5320
5316
  2. Do NOT read files you already know (AGENTS.md, CLAUDE.md, mistflow.json, middleware.ts, lib/auth.ts, lib/db.ts).
@@ -5400,9 +5396,9 @@ child.on('error', (err) => {
5400
5396
  `).slice(-e).map(i=>`[err] ${i}`)),n.filter(i=>i.trim().length>0).join(`
5401
5397
  `)}function Dt(t){return{stdout:he(pt(t),"stdout.log"),stderr:he(pt(t),"stderr.log")}}async function po(t){let e=Sc(t);if(!e)return null;let r=e;return(e.status==="running"||e.status==="starting")&&!Pc(e)&&(r={...e,status:"unknown_exit",endedAt:e.endedAt??new Date().toISOString()}),{...r,elapsed:Ic(r.startedAt,r.endedAt),logTail:Cc(t)}}var Nc=Sr.object({projectPath:Sr.string().optional().describe("Absolute path to the project. Required for the first call (to start the install)."),jobId:Sr.string().optional().describe("Job ID returned from a previous call. Present means poll; absent means start.")}).refine(t=>!!t.projectPath||!!t.jobId,{message:"Pass projectPath to start an install, or jobId to poll an existing one."}),Es={name:"mist_install",description:"Install a Mistflow project's npm dependencies with the fire-and-poll pattern. First call with { projectPath } starts the install and returns a jobId. Subsequent calls with { jobId } poll for progress (status: 'running' | 'complete' | 'failed'). Re-call while status is 'running' \u2014 each response is <1s so there is no 60s timeout risk.",inputSchema:Nc,handler:async t=>{let e=t;if(e.jobId){let a=await po(e.jobId);if(!a)return c(JSON.stringify({status:"not_found",jobId:e.jobId,message:`No install job found for jobId '${e.jobId}'. Start a new one with { projectPath }.`}),!0);if(a.status==="running"||a.status==="starting")return c(JSON.stringify({status:"running",jobId:a.id,elapsed:a.elapsed,logTail:a.logTail,nextAction:"Still running. Call mist_install with the same jobId in ~15-30s to check progress."}));if(a.status==="complete")return c(JSON.stringify({status:"complete",jobId:a.id,elapsed:a.elapsed,exitCode:a.exitCode,nextAction:"Dependencies installed. Run mist_implement next to start executing plan steps."}));let l=Dt(a.id);return c(JSON.stringify({status:a.status,jobId:a.id,elapsed:a.elapsed,exitCode:a.exitCode,logTail:a.logTail,logPaths:l,nextAction:`npm install failed. Read the logTail for the error and fix it \u2014 usually a missing native dep or a version conflict. If the 200-line tail is truncated, read the full log from logPaths.stderr (${l.stderr}) with your file-read tool. After fixing, start a new install with { projectPath }.`}),!0)}let r=Ac(e.projectPath);if(!_c(Rc(r,"package.json")))return c(`No package.json at ${r}. Confirm the path and that mist_init has scaffolded the project.`,!0);let i=`npm install && (${`npx --yes shadcn@latest add -y -o ${["button","card","input","label","form","dialog","table","dropdown-menu","badge","separator","skeleton","sheet","tabs","avatar","select","textarea","checkbox","switch","tooltip","popover","sonner"].join(" ")}`} || echo 'shadcn add failed \u2014 implement will retry lazily')`,s=await co({type:"install",cmd:"sh",args:["-c",i],cwd:r,env:{NPM_CONFIG_LEGACY_PEER_DEPS:"true"}});return c(JSON.stringify({status:"running",jobId:s.id,startedAt:s.startedAt,cwd:r,nextAction:"Install started in the background (npm install + shadcn components). Call mist_install again with { jobId: '"+s.id+"' } in ~15-30s to check progress. Typical duration: 30-90s."}))}};import{z as uo}from"zod";import{resolve as Ec,join as Ce}from"path";import{existsSync as Ae,readFileSync as Ds,statSync as Dc}from"fs";var js=10*1024;function jc(t){if(!(Ae(Ce(t,"open-next.config.ts"))||Ae(Ce(t,"open-next.config.js"))))return null;let r=Ce(t,".open-next","worker.js");if(!Ae(r))return`Build exited 0 but .open-next/worker.js is missing at ${r}. The Cloudflare adapter did not produce a worker bundle. Common causes: (1) open-next.config.ts is misconfigured, (2) a client component imports a Node-only API (fs, path, crypto node:* imports in 'use client' files), (3) the adapter crashed silently mid-build. Check the logTail above for "error"/"failed" messages, fix the offending file, and re-run mist_build.`;try{let o=Dc(r).size;if(o<js)return`Build exited 0 but .open-next/worker.js is only ${o} bytes (expected at least ${js}). The worker bundle is too small to be a working app \u2014 the adapter likely failed to bundle most of the code. Check open-next.config.ts and the logTail above for errors, fix them, and re-run mist_build.`}catch(o){return`Could not stat .open-next/worker.js: ${o instanceof Error?o.message:String(o)}. Re-run mist_build.`}return null}var Oc=uo.object({projectPath:uo.string().optional().describe("Absolute path to the project. Required for the first call."),jobId:uo.string().optional().describe("Job ID from a previous call. Present means poll; absent means start."),script:uo.string().optional().describe("Optional override: run `npm run <script>` instead of the Cloudflare adapter. Default behavior is `npx @opennextjs/cloudflare build` when open-next.config.ts exists, else `npm run build`. Ignored on poll calls.")}).refine(t=>!!t.projectPath||!!t.jobId,{message:"Pass projectPath to start a build, or jobId to poll an existing one."});function Mc(t){let e=Ce(lo(),t,"stdout.log"),r=Ce(lo(),t,"stderr.log"),o="";try{Ae(e)&&(o+=Ds(e,"utf-8"))}catch{}try{Ae(r)&&(o+=`
5402
5398
  `+Ds(r,"utf-8"))}catch{}return o}function Lc(t){let e=/Module not found:\s*(?:Error:\s*)?Can't resolve ['"]([^'"]+)['"]/g,r=new Set;for(let o of t.matchAll(e)){let n=o[1];if(n.startsWith(".")||n.startsWith("@/")||n.startsWith("~/"))continue;let i=n.startsWith("@")?n.split("/").slice(0,2).join("/"):n.split("/")[0];i&&r.add(i)}return[...r]}var Os={name:"mist_build",description:"Run a Mistflow project's production build with the fire-and-poll pattern. First call with { projectPath } starts `npm run build` and returns a jobId. Subsequent calls with { jobId } poll status. On failure, the response surfaces structured errors (from parseBuildErrors) and any missingModules[] \u2014 chain into mist_install with those modules, then mist_build again, without asking the user.",inputSchema:Oc,handler:async t=>{let e=t;if(e.jobId){let l=await po(e.jobId);if(!l)return c(JSON.stringify({status:"not_found",jobId:e.jobId,message:`No build job found for jobId '${e.jobId}'. Start a new one with { projectPath }.`}),!0);if(l.status==="running"||l.status==="starting")return c(JSON.stringify({status:"running",jobId:l.id,elapsed:l.elapsed,logTail:l.logTail,nextAction:"Build still running. Call mist_build again with the same jobId in ~20-40s."}));if(l.status==="complete"){let b=jc(l.cwd);if(b){let f=Dt(l.id);return c(JSON.stringify({status:"failed",jobId:l.id,elapsed:l.elapsed,exitCode:l.exitCode,logTail:l.logTail,logPaths:f,error:b,nextAction:b+` Full build log: ${f.stdout} and ${f.stderr}.`}),!0)}return c(JSON.stringify({status:"complete",jobId:l.id,elapsed:l.elapsed,exitCode:l.exitCode,nextAction:"Build passed. Run mist_deploy next to ship \u2014 do NOT ask the user to confirm, the build is the approval gate."}))}let d=Mc(l.id),h=Zt(d),m=Lc(d),p=Dt(l.id),y=` Full log: ${p.stdout} and ${p.stderr}.`,x=m.length>0?`Build failed with missing modules: ${m.join(", ")}. Call mist_install with these packages (or chain mist_install { projectPath } to reinstall all), then call mist_build again. Do NOT ask the user.${y}`:h.length>0?`Build failed with structured errors (see errors[]). Fix the code, then call mist_build again.${y}`:`Build failed. Read the logTail for the error, or call mist_debug with the failed jobId to extract structured errors.${y}`;return c(JSON.stringify({status:l.status,jobId:l.id,elapsed:l.elapsed,exitCode:l.exitCode,errors:h,missingModules:m,logTail:l.logTail,logPaths:p,nextAction:x}),!0)}let r=Ec(e.projectPath);if(!Ae(Ce(r,"package.json")))return c(`No package.json at ${r}. Run mist_init first.`,!0);if(!Ae(Ce(r,"node_modules")))return c(`node_modules not installed. Run mist_install { projectPath: '${r}' } first.`,!0);let o,n,i,s=Ae(Ce(r,"open-next.config.ts"))||Ae(Ce(r,"open-next.config.js"));e.script?(o="npm",n=["run",e.script],i=e.script):s?(o="npx",n=["-y","@opennextjs/cloudflare","build"],i="@opennextjs/cloudflare build"):(o="npm",n=["run","build"],i="build");let a=await co({type:"build",cmd:o,args:n,cwd:r});return c(JSON.stringify({status:"running",jobId:a.id,startedAt:a.startedAt,cwd:r,script:i,nextAction:`Build started. Call mist_build again with { jobId: '${a.id}' } in ~30s to check progress. Typical duration: 30-90s on a fresh scaffold (Cloudflare adapter), 10-30s on subsequent builds.`}))}};import{existsSync as Uc,readFileSync as $c}from"fs";import{join as Fc}from"path";import{z as mo}from"zod";pe();var qc=mo.object({projectPath:mo.string().optional().describe("Absolute path to the Mistflow project. Defaults to the current working directory. Used to read mistflow.json for the deploy URL + projectId."),url:mo.string().optional().describe("Override the deploy URL to QA. Useful for previews or when mistflow.json doesn't yet have the URL recorded. Defaults to mistflow.json's deploy.url."),deploymentId:mo.string().optional().describe("Deployment ID to associate QA results with. When set, results are uploaded to the backend and surfaced on the dashboard deployment card.")}),Us={name:"mist_qa",description:"Post-deploy QA: drives Playwright in-process against the live deploy URL. Checks health + auth endpoints, logs in with a seeded admin account, walks the dashboard and sidebar pages, and runs design-quality audits (typography, color, layout, slop patterns). Returns pass/fail + screenshots inline. Call ONCE after mist_deploy completes. Do NOT surface the deploy URL to the user until mist_qa returns status: 'pass'.",inputSchema:qc,handler:async t=>Hc(t)};function Bc(t){let e=Fc(t,"mistflow.json");if(!Uc(e))return null;try{return JSON.parse($c(e,"utf-8"))}catch{return null}}async function Ms(t){try{let e=await fetch(t,{redirect:"follow",signal:AbortSignal.timeout(15e3)}),r=await e.text();return{status:e.status,body:r}}catch(e){return{status:0,body:String(e)}}}async function zc(t,e,r){try{let o=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json",...r??{}},body:JSON.stringify(e),redirect:"follow",signal:AbortSignal.timeout(15e3)}),n=await o.text(),i;try{i=JSON.parse(n)}catch{}return{status:o.status,json:i}}catch{return{status:0}}}async function Ls(t){try{let e=await t.screenshot({type:"png"});return Buffer.from(e).toString("base64")}catch{return}}async function ut(t,e,r){let o=[],n=i=>{i.type()==="error"&&o.push(i.text())};t.on("console",n);try{let i=await r(),s=await Ls(t);return{name:e,status:i.pass?"pass":"fail",detail:i.detail,fix:i.fix,screenshot:s,consoleErrors:o.length>0?o:void 0}}catch(i){let s=await Ls(t);return{name:e,status:"fail",detail:`Unexpected error: ${i instanceof Error?i.message:String(i)}`,screenshot:s,consoleErrors:o.length>0?o:void 0}}finally{t.removeListener("console",n)}}async function Hc(t){let e=t.projectPath??process.cwd(),r=Bc(e),o=t.url;if(o||(o=r?.deploy?.url),!o)return c("No deploy URL found. Deploy the app first with mist_deploy, then call mist_qa.",!0);o.startsWith("http")||(o=`https://${o}`);let n=r?.projectId,i=[],s=await Ms(`${o}/api/health`);if(s.status!==200)return i.push({name:"Health endpoint",status:"fail",detail:`Returns ${s.status}`,fix:"The worker is not running or crashed on startup. Check app/api/health/route.ts exists and the build succeeded."}),ho(o,i);i.push({name:"Health endpoint",status:"pass",detail:"Returns 200"});let a=await Ms(`${o}/api/auth/ok`);if(a.status!==200)return i.push({name:"Auth system",status:"fail",detail:`Auth endpoint returns ${a.status}`,fix:"Better Auth is not working. Check lib/auth.ts, lib/db.ts, and that your database env vars are set."}),ho(o,i);i.push({name:"Auth system",status:"pass",detail:"Better Auth running"});let l,d,h;if(n){let y=await Oo(n);if(y){console.error("[qa] Calling seed endpoint for session token");let x=await zc(`${o}/api/admin/seed`,{token:y.seedToken,email:y.email,password:"QaTemp1!"});x.status===200&&x.json?(l=x.json.sessionToken,d=x.json.email,x.json.seeded?(h=x.json.password,console.error("[qa] New admin seeded \u2014 login form test available")):console.error("[qa] Admin already exists \u2014 session injection only")):console.error(`[qa] Seed endpoint returned ${x.status}`)}}if(!l)return i.push({name:"Auth session",status:"fail",detail:"Could not acquire a session token from the seed endpoint",fix:"Redeploy the app with mist_deploy. The deploy process injects ADMIN_SEED_TOKEN into the worker env and stores the seed token on the backend. If the seed endpoint at /api/admin/seed is missing, ensure app/api/admin/seed/route.ts exists in the scaffold."}),ho(o,i);let m,p;try{let{getIsolatedContext:y}=await Promise.resolve().then(()=>($t(),To)),x=await y();m=x.context,p=x.page}catch(y){let x=y instanceof Error?y.message:String(y);return c(JSON.stringify({status:"cannot_verify",url:o,deployed:!0,reason:"App deployed successfully, but QA could not verify it via automated browser testing because Playwright is not installed locally.",detail:x,httpChecks:i.map(({screenshot:b,...f})=>f),fix:"Run: npx playwright install chromium",instruction:[`The app deployed and is live at ${o}. That part succeeded.`,"What DID NOT succeed: automated QA verification. Playwright is not installed locally, so we could not open the app in a browser and check that the landing renders, signup works, and the dashboard loads.","Tell the user BOTH facts clearly \u2014 don't conflate 'deployed' with 'verified':",` "Your app is live at ${o}. I couldn't run automated QA because Playwright isn't installed locally. Run \`npx playwright install chromium\` (one-time, ~150MB) and I'll verify it for you \u2014 or just open the URL and try it yourself."`,"HTTP checks (health/auth endpoints) passed \u2014 those prove the server is responding. They do not prove the UI renders or user flows work.","After Playwright is installed, call mist_qa again for full verification."].join(`
5403
- `)}),!1)}try{let y=await ut(p,"Landing page",async()=>{await p.goto(o,{waitUntil:"domcontentloaded",timeout:3e4}),await p.waitForLoadState("networkidle").catch(()=>{});let b=await p.evaluate(()=>{let S=document.body;if(!S)return"";let A=document.createTreeWalker(S,NodeFilter.SHOW_TEXT),E="",T;for(;T=A.nextNode();){let j=T.parentElement;if(j){let L=window.getComputedStyle(j);L.display!=="none"&&L.visibility!=="hidden"&&parseFloat(L.opacity)>0&&(E+=T.textContent?.trim()+" ")}}return E.trim()});if(b.length<50)return{pass:!1,detail:`Landing page appears blank (${b.length} chars visible). Likely a CSS/JS rendering issue.`,fix:"Common cause: motion/react animations with opacity:0 and whileInView that never trigger on Mistflow Cloud's edge runtime (no Intersection Observer). Replace with CSS animations or set initial={{ opacity: 1 }}."};let f=p.url();return f.includes("/login")||f.includes("/sign-in")?{pass:!1,detail:"Root URL redirects to login instead of showing a landing page",fix:"Check middleware.ts: '/' must be in PUBLIC_EXACT. Check app/page.tsx: must be a landing page, not a redirect."}:{pass:!0,detail:`Renders visible content (${b.length} chars)`}});i.push(y);let x=!1;if(h){let b=await ut(p,"Login",async()=>{await p.goto(`${o}/login`,{waitUntil:"domcontentloaded",timeout:15e3}),await p.waitForLoadState("networkidle").catch(()=>{});let f=p.locator('input[type="email"], input[name="email"], input[placeholder*="email" i]'),S=p.locator('input[type="password"], input[name="password"]');try{await f.first().waitFor({state:"visible",timeout:1e4})}catch{return{pass:!1,detail:"Login page has no visible email input field",fix:"Check app/(auth)/login/page.tsx renders a login form with email and password inputs."}}await f.first().fill(d),await S.first().fill(h),await p.locator('button[type="submit"], button:has-text("Sign in"), button:has-text("Log in"), button:has-text("Login")').first().click();try{await p.waitForURL(E=>!E.pathname.includes("/login"),{timeout:1e4})}catch{let E=await p.locator('[role="alert"], .text-red-500, .text-destructive, [data-error]').first().textContent().catch(()=>null);return{pass:!1,detail:E?`Login failed: ${E}`:"Login did not redirect. Page stayed on /login.",fix:"Do NOT edit lib/auth.ts to disable email verification. Redeploy with mist_deploy to re-seed the verified admin account."}}return x=!0,{pass:!0,detail:`Logged in, redirected to ${p.url()}`}});i.push(b)}else i.push({name:"Login",status:"pass",detail:"Skipped form login (redeploy, password unavailable). Using session injection."});if(!x&&l){let b=new URL(o).hostname,f=o.startsWith("https");await m.addCookies([{name:f?"__Secure-better-auth.session_token":"better-auth.session_token",value:l,domain:b,path:"/",httpOnly:!0,secure:f,sameSite:"Lax"}]),console.error("[qa] Injected session cookie for dashboard checks")}{let b=await ut(p,"Dashboard",async()=>{p.url().includes("/dashboard")||(await p.goto(`${o}/dashboard`,{waitUntil:"domcontentloaded",timeout:15e3}),await p.waitForLoadState("networkidle").catch(()=>{}));let E=await p.content();return E.length<1e3?{pass:!1,detail:`Dashboard page is very small (${E.length} bytes)`,fix:"Check app/(dashboard)/dashboard/page.tsx exists and the dashboard layout doesn't crash."}:await p.locator('text="Something went wrong"').isVisible().catch(()=>!1)?{pass:!1,detail:"Dashboard shows error boundary",fix:"A server component crashed. Check the page.tsx for unhandled null/undefined or missing database tables."}:{pass:!0,detail:`Loads (${E.length} bytes)`}});i.push(b);let f=await p.evaluate(()=>{let A=[];return document.querySelectorAll("nav a[href], aside a[href]").forEach(T=>{let j=T.getAttribute("href");j&&j.startsWith("/")&&!j.startsWith("/api")&&!j.includes("[")&&j!=="/dashboard"&&j!=="/"&&!j.includes("/login")&&!j.includes("/sign")&&A.push(j)}),[...new Set(A)]});if(f.length>0){let A=0,E=[];for(let T of f.slice(0,8)){let j=await ut(p,`Page: ${T}`,async()=>{await p.goto(`${o}${T}`,{waitUntil:"domcontentloaded",timeout:15e3}),await p.waitForLoadState("networkidle").catch(()=>{});let L=await p.title(),v=await p.content();return L.toLowerCase().includes("500")||L.toLowerCase().includes("server error")?{pass:!1,detail:"Page returns 500 server error",fix:"Server component crashed. Common causes: 1) Database tables missing. 2) Wrong ORM dialect (pgTable vs sqliteTable). 3) Unhandled null/undefined in server component."}:L.toLowerCase().includes("404")||L.toLowerCase().includes("not found")?{pass:!1,detail:"Page returns 404",fix:`Page ${T} not found. Create the page or remove the nav link.`}:await p.locator('text="Something went wrong"').isVisible().catch(()=>!1)?{pass:!1,detail:"Page shows error boundary",fix:"A server component crashed. Check the page.tsx for unhandled errors."}:v.length<500?{pass:!1,detail:`Page is very small (${v.length} bytes)`,fix:"Page may not have rendered. Check the page component."}:{pass:!0,detail:"Loads without errors"}});j.status==="fail"&&(A++,E.push(T)),i.push(j)}}let S=await ut(p,"Design quality",async()=>{let A=p.url().includes("/dashboard")?p.url():`${o}/dashboard`;p.url().includes("/dashboard")||(await p.goto(A,{waitUntil:"domcontentloaded",timeout:15e3}),await p.waitForLoadState("networkidle").catch(()=>{}));let E=await p.evaluate(()=>{let T=[],j=document.querySelectorAll("h1, h2, h3, h4, h5, h6"),L=new Set;j.forEach(R=>{L.add(window.getComputedStyle(R).fontSize)}),j.length>=3&&L.size<2&&T.push("TYPOGRAPHY: All headings appear the same size. Create clear hierarchy with 3+ distinct sizes using a modular scale (1.25-1.5x ratio).");let v=Array.from(j).map(R=>parseInt(R.tagName.charAt(1),10));for(let R=1;R<v.length;R++)if(v[R]-v[R-1]>1){T.push(`TYPOGRAPHY: Heading level skipped (h${v[R-1]} -> h${v[R]}). Use sequential heading levels for accessibility.`);break}let M=document.querySelectorAll("*"),H=!1,W=!1;M.forEach(R=>{let z=window.getComputedStyle(R);z.backgroundColor==="rgb(0, 0, 0)"&&R.clientHeight>100&&R.clientWidth>200&&(H=!0);let ge=z.backgroundColor,G=z.color;if(ge&&!ge.includes("0, 0, 0")&&!ge.includes("255, 255, 255")&&ge!=="rgba(0, 0, 0, 0)"&&ge!=="transparent"&&G.match(/rgb\((\d+), (\d+), (\d+)\)/)){let ke=G.match(/rgb\((\d+), (\d+), (\d+)\)/);if(ke){let[Fe,qe,Be]=[parseInt(ke[1]),parseInt(ke[2]),parseInt(ke[3])],me=Math.abs(Fe-qe)<10&&Math.abs(qe-Be)<10&&Fe>80&&Fe<180,_e=ge.match(/rgb\((\d+), (\d+), (\d+)\)/);if(me&&_e){let[Re,Ne,w]=[parseInt(_e[1]),parseInt(_e[2]),parseInt(_e[3])];!(Math.abs(Re-Ne)<15&&Math.abs(Ne-w)<15)&&R.textContent&&R.textContent.trim().length>0&&(W=!0)}}}}),H&&T.push("COLOR: Pure black (#000) background detected on a large element. Use a tinted dark color instead (e.g. oklch(15% 0.01 hue) or a deep navy/charcoal)."),W&&T.push("COLOR: Gray text on a colored background detected. Gray looks washed out on color. Use a darker shade of the background color or white instead.");let oe=document.querySelectorAll('[class*="card"], [class*="Card"], [role="group"]'),N=!1;oe.forEach(R=>{R.querySelectorAll('[class*="card"], [class*="Card"]').length>0&&(N=!0)}),N&&T.push("LAYOUT: Nested cards detected (card inside card). Flatten the hierarchy. Use spacing and background color to create separation instead.");let _=document.querySelectorAll("p, li, span, div"),re=0,ce=0;_.forEach(R=>{R.textContent&&R.textContent.trim().length>20&&R.clientHeight>0&&(ce++,window.getComputedStyle(R).textAlign==="center"&&re++)}),ce>5&&re/ce>.7&&T.push("LAYOUT: Most text is center-aligned. Use left-alignment for body content and lists. Reserve center-alignment for heroes and CTAs.");let D=new Set;M.forEach(R=>{let z=window.getComputedStyle(R);z.gap&&z.gap!=="normal"&&z.gap!=="0px"&&D.add(z.gap)}),D.size===1&&M.length>20&&T.push("LAYOUT: Same gap value used everywhere. Vary spacing to create hierarchy: tight within groups (8-12px), generous between sections (32-64px).");let Q=document.querySelectorAll("button, a, input, select, textarea");document.querySelectorAll("table, [role='table']").forEach(R=>{R.querySelectorAll("tbody tr").length===0&&(R.parentElement?.querySelector('[class*="empty"], [class*="Empty"], [class*="no-data"], [class*="placeholder"]')||T.push("UX: Empty table with no empty state. Add a helpful message explaining what will appear here and a CTA to create the first item."))});let ue=document.querySelectorAll("style"),Te=!1;ue.forEach(R=>{let z=R.textContent||"";(z.includes("bounce")||z.includes("elastic")||z.match(/cubic-bezier\([^)]*[2-9]\.[0-9]/))&&(Te=!0)}),Te&&T.push("MOTION: Bounce or elastic easing detected. These feel dated. Use smooth deceleration curves (Quart out, Expo out) instead.");let $e=document.querySelectorAll("button, a, input, select, [role='button']"),F=0;return $e.forEach(R=>{let z=R.getBoundingClientRect();z.width>0&&z.height>0&&(z.width<32||z.height<32)&&F++}),F>3&&T.push(`ACCESSIBILITY: ${F} interactive elements are smaller than 32x32px. Minimum recommended touch target is 44x44px. Add padding to increase tap area.`),T});return E.length===0?{pass:!0,detail:"No design quality issues detected. Typography hierarchy, color usage, layout patterns, and accessibility basics look good."}:{pass:!1,detail:`${E.length} design quality issue(s) found:
5399
+ `)}),!1)}try{let y=await ut(p,"Landing page",async()=>{await p.goto(o,{waitUntil:"domcontentloaded",timeout:3e4}),await p.waitForLoadState("networkidle").catch(()=>{});let b=await p.evaluate(()=>{let S=document.body;if(!S)return"";let A=document.createTreeWalker(S,NodeFilter.SHOW_TEXT),E="",T;for(;T=A.nextNode();){let j=T.parentElement;if(j){let L=window.getComputedStyle(j);L.display!=="none"&&L.visibility!=="hidden"&&parseFloat(L.opacity)>0&&(E+=T.textContent?.trim()+" ")}}return E.trim()});if(b.length<50)return{pass:!1,detail:`Landing page appears blank (${b.length} chars visible). Likely a CSS/JS rendering issue.`,fix:"Common cause: motion/react animations with opacity:0 and whileInView that never trigger on Mistflow Cloud's edge runtime (no Intersection Observer). Replace with CSS animations or set initial={{ opacity: 1 }}."};let f=p.url();return f.includes("/login")||f.includes("/sign-in")?{pass:!1,detail:"Root URL redirects to login instead of showing a landing page",fix:"Check middleware.ts: '/' must be in PUBLIC_EXACT. Check app/page.tsx: must be a landing page, not a redirect."}:{pass:!0,detail:`Renders visible content (${b.length} chars)`}});i.push(y);let x=!1;if(h){let b=await ut(p,"Login",async()=>{await p.goto(`${o}/login`,{waitUntil:"domcontentloaded",timeout:15e3}),await p.waitForLoadState("networkidle").catch(()=>{});let f=p.locator('input[type="email"], input[name="email"], input[placeholder*="email" i]'),S=p.locator('input[type="password"], input[name="password"]');try{await f.first().waitFor({state:"visible",timeout:1e4})}catch{return{pass:!1,detail:"Login page has no visible email input field",fix:"Check app/(auth)/login/page.tsx renders a login form with email and password inputs."}}await f.first().fill(d),await S.first().fill(h),await p.locator('button[type="submit"], button:has-text("Sign in"), button:has-text("Log in"), button:has-text("Login")').first().click();try{await p.waitForURL(E=>!E.pathname.includes("/login"),{timeout:1e4})}catch{let E=await p.locator('[role="alert"], .text-red-500, .text-destructive, [data-error]').first().textContent().catch(()=>null);return{pass:!1,detail:E?`Login failed: ${E}`:"Login did not redirect. Page stayed on /login.",fix:"Do NOT edit lib/auth.ts to disable email verification. Redeploy with mist_deploy to re-seed the verified admin account."}}return x=!0,{pass:!0,detail:`Logged in, redirected to ${p.url()}`}});i.push(b)}else i.push({name:"Login",status:"pass",detail:"Skipped form login (redeploy, password unavailable). Using session injection."});if(!x&&l){let b=new URL(o).hostname,f=o.startsWith("https");await m.addCookies([{name:f?"__Secure-better-auth.session_token":"better-auth.session_token",value:l,domain:b,path:"/",httpOnly:!0,secure:f,sameSite:"Lax"}]),console.error("[qa] Injected session cookie for dashboard checks")}{let b=await ut(p,"Dashboard",async()=>{p.url().includes("/dashboard")||(await p.goto(`${o}/dashboard`,{waitUntil:"domcontentloaded",timeout:15e3}),await p.waitForLoadState("networkidle").catch(()=>{}));let E=await p.content();return E.length<1e3?{pass:!1,detail:`Dashboard page is very small (${E.length} bytes)`,fix:"Check app/(dashboard)/dashboard/page.tsx exists and the dashboard layout doesn't crash."}:await p.locator('text="Something went wrong"').isVisible().catch(()=>!1)?{pass:!1,detail:"Dashboard shows error boundary",fix:"A server component crashed. Check the page.tsx for unhandled null/undefined or missing database tables."}:{pass:!0,detail:`Loads (${E.length} bytes)`}});i.push(b);let f=await p.evaluate(()=>{let A=[];return document.querySelectorAll("nav a[href], aside a[href]").forEach(T=>{let j=T.getAttribute("href");j&&j.startsWith("/")&&!j.startsWith("/api")&&!j.includes("[")&&j!=="/dashboard"&&j!=="/"&&!j.includes("/login")&&!j.includes("/sign")&&A.push(j)}),[...new Set(A)]});if(f.length>0){let A=0,E=[];for(let T of f.slice(0,8)){let j=await ut(p,`Page: ${T}`,async()=>{await p.goto(`${o}${T}`,{waitUntil:"domcontentloaded",timeout:15e3}),await p.waitForLoadState("networkidle").catch(()=>{});let L=await p.title(),v=await p.content();return L.toLowerCase().includes("500")||L.toLowerCase().includes("server error")?{pass:!1,detail:"Page returns 500 server error",fix:"Server component crashed. Common causes: 1) Database tables missing. 2) Wrong ORM dialect (pgTable vs sqliteTable). 3) Unhandled null/undefined in server component."}:L.toLowerCase().includes("404")||L.toLowerCase().includes("not found")?{pass:!1,detail:"Page returns 404",fix:`Page ${T} not found. Create the page or remove the nav link.`}:await p.locator('text="Something went wrong"').isVisible().catch(()=>!1)?{pass:!1,detail:"Page shows error boundary",fix:"A server component crashed. Check the page.tsx for unhandled errors."}:v.length<500?{pass:!1,detail:`Page is very small (${v.length} bytes)`,fix:"Page may not have rendered. Check the page component."}:{pass:!0,detail:"Loads without errors"}});j.status==="fail"&&(A++,E.push(T)),i.push(j)}}let S=await ut(p,"Design quality",async()=>{let A=p.url().includes("/dashboard")?p.url():`${o}/dashboard`;p.url().includes("/dashboard")||(await p.goto(A,{waitUntil:"domcontentloaded",timeout:15e3}),await p.waitForLoadState("networkidle").catch(()=>{}));let E=await p.evaluate(()=>{let T=[],j=document.querySelectorAll("h1, h2, h3, h4, h5, h6"),L=new Set;j.forEach(R=>{L.add(window.getComputedStyle(R).fontSize)}),j.length>=3&&L.size<2&&T.push("TYPOGRAPHY: All headings appear the same size. Create clear hierarchy with 3+ distinct sizes using a modular scale (1.25-1.5x ratio).");let v=Array.from(j).map(R=>parseInt(R.tagName.charAt(1),10));for(let R=1;R<v.length;R++)if(v[R]-v[R-1]>1){T.push(`TYPOGRAPHY: Heading level skipped (h${v[R-1]} -> h${v[R]}). Use sequential heading levels for accessibility.`);break}let M=document.querySelectorAll("*"),H=!1,W=!1;M.forEach(R=>{let z=window.getComputedStyle(R);z.backgroundColor==="rgb(0, 0, 0)"&&R.clientHeight>100&&R.clientWidth>200&&(H=!0);let ge=z.backgroundColor,G=z.color;if(ge&&!ge.includes("0, 0, 0")&&!ge.includes("255, 255, 255")&&ge!=="rgba(0, 0, 0, 0)"&&ge!=="transparent"&&G.match(/rgb\((\d+), (\d+), (\d+)\)/)){let ke=G.match(/rgb\((\d+), (\d+), (\d+)\)/);if(ke){let[Fe,qe,Be]=[parseInt(ke[1]),parseInt(ke[2]),parseInt(ke[3])],me=Math.abs(Fe-qe)<10&&Math.abs(qe-Be)<10&&Fe>80&&Fe<180,_e=ge.match(/rgb\((\d+), (\d+), (\d+)\)/);if(me&&_e){let[Re,Ne,w]=[parseInt(_e[1]),parseInt(_e[2]),parseInt(_e[3])];!(Math.abs(Re-Ne)<15&&Math.abs(Ne-w)<15)&&R.textContent&&R.textContent.trim().length>0&&(W=!0)}}}}),H&&T.push("COLOR: Pure black (#000) background detected on a large element. Use a tinted dark color instead (e.g. oklch(15% 0.01 hue) or a deep navy/charcoal)."),W&&T.push("COLOR: Gray text on a colored background detected. Gray looks washed out on color. Use a darker shade of the background color or white instead.");let re=document.querySelectorAll('[class*="card"], [class*="Card"], [role="group"]'),N=!1;re.forEach(R=>{R.querySelectorAll('[class*="card"], [class*="Card"]').length>0&&(N=!0)}),N&&T.push("LAYOUT: Nested cards detected (card inside card). Flatten the hierarchy. Use spacing and background color to create separation instead.");let _=document.querySelectorAll("p, li, span, div"),ne=0,ce=0;_.forEach(R=>{R.textContent&&R.textContent.trim().length>20&&R.clientHeight>0&&(ce++,window.getComputedStyle(R).textAlign==="center"&&ne++)}),ce>5&&ne/ce>.7&&T.push("LAYOUT: Most text is center-aligned. Use left-alignment for body content and lists. Reserve center-alignment for heroes and CTAs.");let D=new Set;M.forEach(R=>{let z=window.getComputedStyle(R);z.gap&&z.gap!=="normal"&&z.gap!=="0px"&&D.add(z.gap)}),D.size===1&&M.length>20&&T.push("LAYOUT: Same gap value used everywhere. Vary spacing to create hierarchy: tight within groups (8-12px), generous between sections (32-64px).");let Q=document.querySelectorAll("button, a, input, select, textarea");document.querySelectorAll("table, [role='table']").forEach(R=>{R.querySelectorAll("tbody tr").length===0&&(R.parentElement?.querySelector('[class*="empty"], [class*="Empty"], [class*="no-data"], [class*="placeholder"]')||T.push("UX: Empty table with no empty state. Add a helpful message explaining what will appear here and a CTA to create the first item."))});let ue=document.querySelectorAll("style"),Te=!1;ue.forEach(R=>{let z=R.textContent||"";(z.includes("bounce")||z.includes("elastic")||z.match(/cubic-bezier\([^)]*[2-9]\.[0-9]/))&&(Te=!0)}),Te&&T.push("MOTION: Bounce or elastic easing detected. These feel dated. Use smooth deceleration curves (Quart out, Expo out) instead.");let $e=document.querySelectorAll("button, a, input, select, [role='button']"),F=0;return $e.forEach(R=>{let z=R.getBoundingClientRect();z.width>0&&z.height>0&&(z.width<32||z.height<32)&&F++}),F>3&&T.push(`ACCESSIBILITY: ${F} interactive elements are smaller than 32x32px. Minimum recommended touch target is 44x44px. Add padding to increase tap area.`),T});return E.length===0?{pass:!0,detail:"No design quality issues detected. Typography hierarchy, color usage, layout patterns, and accessibility basics look good."}:{pass:!1,detail:`${E.length} design quality issue(s) found:
5404
5400
  ${E.map((T,j)=>`${j+1}. ${T}`).join(`
5405
- `)}`,fix:"Fix these design issues in the source code. These are common AI-generated design anti-patterns that make apps look generic. Address each issue, then redeploy and re-run QA."}});if(i.push(S),i.find(A=>A.name==="Landing page"&&A.status==="pass")){let A=await ut(p,"Landing design quality",async()=>{await p.goto(o,{waitUntil:"domcontentloaded",timeout:15e3}),await p.waitForLoadState("networkidle").catch(()=>{});let E=await p.evaluate(()=>{let T=[];document.querySelectorAll("*").forEach(M=>{let H=window.getComputedStyle(M);(H.getPropertyValue("-webkit-background-clip")||H.getPropertyValue("background-clip"))==="text"&&M.textContent&&M.textContent.trim().length>0&&T.push("SLOP: Gradient text detected. This is a common AI design pattern. Use solid colors for text.")});let L=document.querySelector("section, [class*='hero'], [class*='Hero'], header + div, main > div:first-child");if(L){let M=(L.textContent||"").toLowerCase(),H=["transform your","unlock the power","revolutionize your","take your .* to the next level","the future of","welcome to","get started today","join thousands","powerful analytics","seamless integration","lightning fast"];for(let W of H)if(M.match(new RegExp(W))){T.push(`COPY: Generic hero text detected ('${W}'). Write specific copy about what THIS app does for its users.`);break}}return document.querySelectorAll('[class*="grid"]').forEach(M=>{let W=window.getComputedStyle(M).gridTemplateColumns;if(W){let oe=W.split(" ").filter(_=>_!=="").length,N=M.children;if(oe===3&&N.length===3){let _=Array.from(N).map(D=>D.offsetHeight),re=_.every(D=>Math.abs(D-_[0])<5),ce=Array.from(N).every(D=>{let Q=D.querySelectorAll("svg"),Se=D.querySelectorAll("h2, h3, h4"),ue=D.querySelectorAll("p");return Q.length>=1&&Se.length>=1&&ue.length>=1});re&&ce&&T.push("SLOP: 3-column icon + title + paragraph feature grid detected. This is the most common AI layout pattern. Use asymmetric layouts, bento grids, or varied card sizes instead.")}}}),T});return E.length===0?{pass:!0,detail:"Landing page design looks intentional. No generic AI patterns detected."}:{pass:!1,detail:`${E.length} landing design issue(s):
5401
+ `)}`,fix:"Fix these design issues in the source code. These are common AI-generated design anti-patterns that make apps look generic. Address each issue, then redeploy and re-run QA."}});if(i.push(S),i.find(A=>A.name==="Landing page"&&A.status==="pass")){let A=await ut(p,"Landing design quality",async()=>{await p.goto(o,{waitUntil:"domcontentloaded",timeout:15e3}),await p.waitForLoadState("networkidle").catch(()=>{});let E=await p.evaluate(()=>{let T=[];document.querySelectorAll("*").forEach(M=>{let H=window.getComputedStyle(M);(H.getPropertyValue("-webkit-background-clip")||H.getPropertyValue("background-clip"))==="text"&&M.textContent&&M.textContent.trim().length>0&&T.push("SLOP: Gradient text detected. This is a common AI design pattern. Use solid colors for text.")});let L=document.querySelector("section, [class*='hero'], [class*='Hero'], header + div, main > div:first-child");if(L){let M=(L.textContent||"").toLowerCase(),H=["transform your","unlock the power","revolutionize your","take your .* to the next level","the future of","welcome to","get started today","join thousands","powerful analytics","seamless integration","lightning fast"];for(let W of H)if(M.match(new RegExp(W))){T.push(`COPY: Generic hero text detected ('${W}'). Write specific copy about what THIS app does for its users.`);break}}return document.querySelectorAll('[class*="grid"]').forEach(M=>{let W=window.getComputedStyle(M).gridTemplateColumns;if(W){let re=W.split(" ").filter(_=>_!=="").length,N=M.children;if(re===3&&N.length===3){let _=Array.from(N).map(D=>D.offsetHeight),ne=_.every(D=>Math.abs(D-_[0])<5),ce=Array.from(N).every(D=>{let Q=D.querySelectorAll("svg"),Se=D.querySelectorAll("h2, h3, h4"),ue=D.querySelectorAll("p");return Q.length>=1&&Se.length>=1&&ue.length>=1});ne&&ce&&T.push("SLOP: 3-column icon + title + paragraph feature grid detected. This is the most common AI layout pattern. Use asymmetric layouts, bento grids, or varied card sizes instead.")}}}),T});return E.length===0?{pass:!0,detail:"Landing page design looks intentional. No generic AI patterns detected."}:{pass:!1,detail:`${E.length} landing design issue(s):
5406
5402
  ${E.map((T,j)=>`${j+1}. ${T}`).join(`
5407
5403
  `)}`,fix:"These patterns make the landing page look AI-generated. Fix them to create a more distinctive, professional design."}});i.push(A)}}}finally{m&&await m.close().catch(()=>{})}if(t.deploymentId){let y=i.filter(f=>f.status==="fail"),x=i.filter(f=>f.status==="pass"),b=Date.now();await Mo(t.deploymentId,{checks:i.map(({screenshot:f,...S})=>S),overall:y.length===0?"pass":"fail",passed:x.length,failed:y.length,duration_ms:Date.now()-b}).catch(()=>{})}return ho(o,i)}function ho(t,e){let r=e.filter(i=>i.status==="fail"),o=e.filter(i=>i.status==="pass"),n=[];if(r.length===0)n.push({type:"text",text:JSON.stringify({status:"pass",message:`QA passed. All ${e.length} checks OK. The app is working correctly.`,url:t,checks:e.map(({screenshot:i,...s})=>s)})});else{let i=r.map((s,a)=>`${a+1}. **${s.name}**: ${s.detail}
5408
5404
  Fix: ${s.fix}`).join(`