@vaiftech/cli 1.9.9 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,19 @@
1
- import b from'fs';import k from'path';import Z from'dotenv';import ee from'os';import {exec}from'child_process';import L from'ora';import l from'chalk';import $ from'readline';import ue from'pg';import pe from'prettier';Z.config();async function q(a){let t=k.resolve(a);if(!b.existsSync(t))return null;try{let n=b.readFileSync(t,"utf-8"),e=JSON.parse(n);return e.database?.url&&(e.database.url=B(e.database.url)),e.api?.apiKey&&(e.api.apiKey=B(e.api.apiKey)),e}catch{throw new Error(`Failed to parse config file: ${a}`)}}function B(a){return a.replace(/\$\{([^}]+)\}/g,(t,n)=>process.env[n]||t)}var C=k.join(ee.homedir(),".vaif"),_=k.join(C,"auth.json"),R=process.env.VAIF_API_URL||"https://api.vaif.studio";function ae(){b.existsSync(C)||b.mkdirSync(C,{recursive:true});}function G(a){ae(),b.writeFileSync(_,JSON.stringify(a,null,2),"utf-8"),b.chmodSync(_,384);}function V(){if(!b.existsSync(_))return null;try{let a=b.readFileSync(_,"utf-8");return JSON.parse(a)}catch{return null}}function ne(a){let t=$.createInterface({input:process.stdin,output:process.stdout});return new Promise(n=>{t.question(a,e=>{t.close(),n(e);});})}function ie(a){return new Promise(t=>{let n=$.createInterface({input:process.stdin,output:process.stdout});process.stdin;let r=process.stdout.write.bind(process.stdout),i=false;process.stdout.write=((...c)=>i?true:r(...c)),n.question(a,c=>{i=false,process.stdout.write=r,console.log(""),n.close(),t(c);}),i=true;})}function oe(a){let t=process.platform,n;t==="darwin"?n=`open "${a}"`:t==="win32"?n=`start "" "${a}"`:n=`xdg-open "${a}"`,exec(n,e=>{});}function re(a){return new Promise(t=>setTimeout(t,a))}async function se(a){try{let t=await fetch(`${R}/auth/me`,{headers:{Authorization:`Bearer ${a}`}});if(t.ok){let n=await t.json();return {valid:!0,email:n.user?.email||n.email}}return {valid:!1}}catch{return {valid:false}}}async function le(a){let t=L();t.start("Setting up authentication...");let n,e;try{let u=await fetch(`${R}/auth/cli/authorize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})});u.ok||(t.fail("Failed to initiate authentication"),console.log(l.red(`
2
- Could not connect to VAIF API. Please try again later.`)),process.exit(1));let o=await u.json();n=o.code,e=o.url;}catch{t.fail("Failed to connect to VAIF API"),console.log(l.red(`
3
- Could not connect to VAIF API.`)),console.log(l.gray("Check your internet connection or try: vaif login --email")),process.exit(1);}t.stop(),console.log(l.cyan(" Opening browser for authentication...")),console.log(""),console.log(l.gray(" If the browser doesn't open, visit this URL:")),console.log(l.white(` ${e}`)),console.log(""),oe(e),t.start("Waiting for browser authentication...");let r=12e4,i=2e3,c=Date.now();for(;Date.now()-c<r;){await re(i);try{let u=await fetch(`${R}/auth/cli/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:n})});if(!u.ok){let s=await u.json();(s.error==="ExpiredCode"||s.error==="InvalidCode")&&(t.fail("Authentication expired"),console.log(l.red(`
4
- The authentication session expired. Please try again.`)),process.exit(1));continue}let o=await u.json();if(o.ok&&o.accessToken){let s={token:o.accessToken,email:o.user?.email,projectId:a,expiresAt:new Date(Date.now()+o.expiresIn*1e3).toISOString()};G(s),t.succeed("Logged in successfully"),console.log(""),o.user?.email&&console.log(l.green(` Authenticated as: ${o.user.email}`)),console.log(l.gray(` Config saved to: ${_}`)),console.log("");return}}catch{}}t.fail("Authentication timed out"),console.log(l.red(`
5
- Timed out waiting for browser authentication.`)),console.log(l.gray("Try again or use: vaif login --email")),process.exit(1);}async function ce(a){console.log(""),console.log(l.bold("VAIF CLI Login")),console.log(l.gray("Enter your VAIF account credentials")),console.log("");let t=await ne(l.cyan(" Email: "));(!t||t.trim()==="")&&(console.log(l.red(`
6
- No email provided. Login cancelled.`)),process.exit(1));let n=await ie(l.cyan(" Password: "));(!n||n.trim()==="")&&(console.log(l.red(`
7
- No password provided. Login cancelled.`)),process.exit(1));let e=L("Authenticating...").start();try{let r=await fetch(`${R}/auth/cli/login`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:t.trim(),password:n})}),i=await r.json();(!r.ok||!i.ok)&&(e.fail("Login failed"),console.log(l.red(`
8
- ${i.message||"Invalid email or password."}`)),process.exit(1));let c={token:i.accessToken,email:i.user?.email,projectId:a,expiresAt:new Date(Date.now()+i.expiresIn*1e3).toISOString()};G(c),e.succeed("Logged in successfully"),console.log(""),i.user?.email&&console.log(l.green(` Authenticated as: ${i.user.email}`)),console.log(l.gray(` Config saved to: ${_}`)),console.log("");}catch{e.fail("Failed to connect to VAIF API"),console.log(l.red(`
9
- Could not connect to VAIF API. Please try again later.`)),process.exit(1);}}async function Ue(a){console.log(""),console.log(l.bold("Welcome to VAIF CLI")),console.log(l.gray("Authenticate to access your VAIF projects")),console.log(""),a.email?await ce(a.projectId):await le(a.projectId),console.log(l.gray("You can now use VAIF CLI commands like:")),console.log(l.gray(" vaif pull - Pull remote schema")),console.log(l.gray(" vaif push - Push schema changes")),console.log(l.gray(" vaif generate - Generate TypeScript types")),console.log("");}async function De(){b.existsSync(_)?(b.unlinkSync(_),console.log(l.green("Logged out successfully"))):console.log(l.yellow("Not currently logged in"));}async function ke(){let a=V();(!a||!a.token)&&(console.log(l.yellow("Not logged in")),console.log(l.gray("Run `vaif login` to authenticate")),process.exit(1));let t=L("Checking authentication...").start(),{valid:n,email:e}=await se(a.token);n||(t.fail("Session expired"),console.log(l.yellow(`
10
- Your session has expired. Please login again.`)),process.exit(1)),t.succeed("Authenticated"),console.log(""),console.log(l.green(` Email: ${e||a.email||"Unknown"}`)),a.projectId&&console.log(l.green(` Project: ${a.projectId}`)),console.log("");}var me=process.env.VAIF_API_URL||"https://api.vaif.studio";async function fe(a,t){let n=await fetch(`${me}/schema-engine/introspect/${t}`,{headers:{Authorization:`Bearer ${a}`,"Content-Type":"application/json"}});if(!n.ok){let u=await n.text();throw new Error(`API introspection failed: ${u}`)}let e=await n.json();if(!e.ok||!e.schemaExists)throw new Error("Project schema does not exist yet. Push a migration first with `vaif db push`.");let r=new Map,i=[];for(let u of e.tables){let o=u.columns.map(s=>({column_name:s.name,data_type:s.type,is_nullable:s.nullable?"YES":"NO",column_default:s.default,udt_name:s.type,is_identity:s.primaryKey&&s.default?.includes("gen_random_uuid")?"YES":"NO",character_maximum_length:null,numeric_precision:null,numeric_scale:null}));r.set(u.name,o);for(let s of u.foreignKeys)i.push({constraint_name:s.constraintName,table_name:u.name,column_name:s.columnName,foreign_table_name:s.refTable,foreign_column_name:s.refColumn});}return {tables:r,enums:new Map,foreignKeys:i}}async function ge(a,t){let n=await a.query(`
1
+ import v from'fs';import I from'path';import se from'dotenv';import le from'os';import {exec}from'child_process';import j from'ora';import u from'chalk';import H from'readline';import ve from'pg';import Ie from'prettier';se.config();async function N(n){let e=I.resolve(n);if(!v.existsSync(e))return null;try{let a=v.readFileSync(e,"utf-8"),t=JSON.parse(a);return t.database?.url&&(t.database.url=G(t.database.url)),t.api?.apiKey&&(t.api.apiKey=G(t.api.apiKey)),t}catch{throw new Error(`Failed to parse config file: ${n}`)}}function G(n){return n.replace(/\$\{([^}]+)\}/g,(e,a)=>process.env[a]||e)}var V=I.join(le.homedir(),".vaif"),S=I.join(V,"auth.json"),D=process.env.VAIF_API_URL||"https://api.vaif.studio";function de(){v.existsSync(V)||v.mkdirSync(V,{recursive:true});}function W(n){de(),v.writeFileSync(S,JSON.stringify(n,null,2),"utf-8"),v.chmodSync(S,384);}function R(){if(!v.existsSync(S))return null;try{let n=v.readFileSync(S,"utf-8");return JSON.parse(n)}catch{return null}}function ue(n){let e=H.createInterface({input:process.stdin,output:process.stdout});return new Promise(a=>{e.question(n,t=>{e.close(),a(t);});})}function pe(n){return new Promise(e=>{let a=H.createInterface({input:process.stdin,output:process.stdout});process.stdin;let o=process.stdout.write.bind(process.stdout),i=false;process.stdout.write=((...s)=>i?true:o(...s)),a.question(n,s=>{i=false,process.stdout.write=o,console.log(""),a.close(),e(s);}),i=true;})}function me(n){let e=process.platform,a;e==="darwin"?a=`open "${n}"`:e==="win32"?a=`start "" "${n}"`:a=`xdg-open "${n}"`,exec(a,t=>{});}function fe(n){return new Promise(e=>setTimeout(e,n))}async function ge(n){try{let e=await fetch(`${D}/auth/me`,{headers:{Authorization:`Bearer ${n}`}});if(e.ok){let a=await e.json();return {valid:!0,email:a.user?.email||a.email}}return {valid:!1}}catch{return {valid:false}}}async function he(n){let e=j();e.start("Setting up authentication...");let a,t;try{let l=await fetch(`${D}/auth/cli/authorize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})});l.ok||(e.fail("Failed to initiate authentication"),console.log(u.red(`
2
+ Could not connect to VAIF API. Please try again later.`)),process.exit(1));let r=await l.json();a=r.code,t=r.url;}catch{e.fail("Failed to connect to VAIF API"),console.log(u.red(`
3
+ Could not connect to VAIF API.`)),console.log(u.gray("Check your internet connection or try: vaif login --email")),process.exit(1);}e.stop(),console.log(u.cyan(" Opening browser for authentication...")),console.log(""),console.log(u.gray(" If the browser doesn't open, visit this URL:")),console.log(u.white(` ${t}`)),console.log(""),me(t),e.start("Waiting for browser authentication...");let o=12e4,i=2e3,s=Date.now();for(;Date.now()-s<o;){await fe(i);try{let l=await fetch(`${D}/auth/cli/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:a})});if(!l.ok){let c=await l.json();(c.error==="ExpiredCode"||c.error==="InvalidCode")&&(e.fail("Authentication expired"),console.log(u.red(`
4
+ The authentication session expired. Please try again.`)),process.exit(1));continue}let r=await l.json();if(r.ok&&r.accessToken){let c={token:r.accessToken,email:r.user?.email,projectId:n,expiresAt:new Date(Date.now()+r.expiresIn*1e3).toISOString()};W(c),e.succeed("Logged in successfully"),console.log(""),r.user?.email&&console.log(u.green(` Authenticated as: ${r.user.email}`)),console.log(u.gray(` Config saved to: ${S}`)),console.log("");return}}catch{}}e.fail("Authentication timed out"),console.log(u.red(`
5
+ Timed out waiting for browser authentication.`)),console.log(u.gray("Try again or use: vaif login --email")),process.exit(1);}async function ye(n){console.log(""),console.log(u.bold("VAIF CLI Login")),console.log(u.gray("Enter your VAIF account credentials")),console.log("");let e=await ue(u.cyan(" Email: "));(!e||e.trim()==="")&&(console.log(u.red(`
6
+ No email provided. Login cancelled.`)),process.exit(1));let a=await pe(u.cyan(" Password: "));(!a||a.trim()==="")&&(console.log(u.red(`
7
+ No password provided. Login cancelled.`)),process.exit(1));let t=j("Authenticating...").start();try{let o=await fetch(`${D}/auth/cli/login`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:e.trim(),password:a})}),i=await o.json();(!o.ok||!i.ok)&&(t.fail("Login failed"),console.log(u.red(`
8
+ ${i.message||"Invalid email or password."}`)),process.exit(1));let s={token:i.accessToken,email:i.user?.email,projectId:n,expiresAt:new Date(Date.now()+i.expiresIn*1e3).toISOString()};W(s),t.succeed("Logged in successfully"),console.log(""),i.user?.email&&console.log(u.green(` Authenticated as: ${i.user.email}`)),console.log(u.gray(` Config saved to: ${S}`)),console.log("");}catch{t.fail("Failed to connect to VAIF API"),console.log(u.red(`
9
+ Could not connect to VAIF API. Please try again later.`)),process.exit(1);}}async function Qe(n){console.log(""),console.log(u.bold("Welcome to VAIF CLI")),console.log(u.gray("Authenticate to access your VAIF projects")),console.log(""),n.email?await ye(n.projectId):await he(n.projectId),console.log(u.gray("You can now use VAIF CLI commands like:")),console.log(u.gray(" vaif pull - Pull remote schema")),console.log(u.gray(" vaif push - Push schema changes")),console.log(u.gray(" vaif generate - Generate TypeScript types")),console.log("");}async function Ze(){v.existsSync(S)?(v.unlinkSync(S),console.log(u.green("Logged out successfully"))):console.log(u.yellow("Not currently logged in"));}async function et(){let n=R();(!n||!n.token)&&(console.log(u.yellow("Not logged in")),console.log(u.gray("Run `vaif login` to authenticate")),process.exit(1));let e=j("Checking authentication...").start(),{valid:a,email:t}=await ge(n.token);a||(e.fail("Session expired"),console.log(u.yellow(`
10
+ Your session has expired. Please login again.`)),process.exit(1)),e.succeed("Authenticated"),console.log(""),console.log(u.green(` Email: ${t||n.email||"Unknown"}`)),n.projectId&&console.log(u.green(` Project: ${n.projectId}`)),console.log("");}var _e=process.env.VAIF_API_URL||"https://api.vaif.studio";async function Ee(n,e){let a=await fetch(`${_e}/schema-engine/introspect/${e}`,{headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json"}});if(!a.ok){let l=await a.text();throw new Error(`API introspection failed: ${l}`)}let t=await a.json();if(!t.ok||!t.schemaExists)throw new Error("Project schema does not exist yet. Push a migration first with `vaif db push`.");let o=new Map,i=[];for(let l of t.tables){let r=l.columns.map(c=>({column_name:c.name,data_type:c.type,is_nullable:c.nullable?"YES":"NO",column_default:c.default,udt_name:c.type,is_identity:c.primaryKey&&c.default?.includes("gen_random_uuid")?"YES":"NO",character_maximum_length:null,numeric_precision:null,numeric_scale:null}));o.set(l.name,r);for(let c of l.foreignKeys)i.push({constraint_name:c.constraintName,table_name:l.name,column_name:c.columnName,foreign_table_name:c.refTable,foreign_column_name:c.refColumn});}return {tables:o,enums:new Map,foreignKeys:i}}async function Ae(n,e){let a=await n.query(`
11
11
  SELECT table_name, table_type
12
12
  FROM information_schema.tables
13
13
  WHERE table_schema = $1
14
14
  AND table_type = 'BASE TABLE'
15
15
  ORDER BY table_name
16
- `,[t]),e=await a.query(`
16
+ `,[e]),t=await n.query(`
17
17
  SELECT
18
18
  table_name,
19
19
  column_name,
@@ -28,7 +28,7 @@ Your session has expired. Please login again.`)),process.exit(1)),t.succeed("Aut
28
28
  FROM information_schema.columns
29
29
  WHERE table_schema = $1
30
30
  ORDER BY table_name, ordinal_position
31
- `,[t]),r=await a.query(`
31
+ `,[e]),o=await n.query(`
32
32
  SELECT
33
33
  tc.constraint_name,
34
34
  tc.table_name,
@@ -44,7 +44,7 @@ Your session has expired. Please login again.`)),process.exit(1)),t.succeed("Aut
44
44
  AND ccu.table_schema = tc.table_schema
45
45
  WHERE tc.constraint_type = 'FOREIGN KEY'
46
46
  AND tc.table_schema = $1
47
- `,[t]),i=await a.query(`
47
+ `,[e]),i=await n.query(`
48
48
  SELECT
49
49
  t.typname as enum_name,
50
50
  e.enumlabel as enum_value
@@ -53,24 +53,24 @@ Your session has expired. Please login again.`)),process.exit(1)),t.succeed("Aut
53
53
  JOIN pg_namespace n ON n.oid = t.typnamespace
54
54
  WHERE n.nspname = $1
55
55
  ORDER BY t.typname, e.enumsortorder
56
- `,[t]),c=new Map;for(let o of n.rows)c.set(o.table_name,[]);for(let o of e.rows){let s=c.get(o.table_name);s&&s.push(o);}let u=new Map;for(let o of i.rows){let s=u.get(o.enum_name)||[];s.push(o.enum_value),u.set(o.enum_name,s);}return {tables:c,enums:u,foreignKeys:r.rows}}var U={smallint:"number",integer:"number",bigint:"string",int2:"number",int4:"number",int8:"string",decimal:"string",numeric:"string",real:"number",float4:"number",float8:"number","double precision":"number",money:"string",boolean:"boolean",bool:"boolean",text:"string",varchar:"string",char:"string",character:"string","character varying":"string",name:"string",citext:"string",date:"string",time:"string",timetz:"string","time without time zone":"string","time with time zone":"string",timestamp:"string",timestamptz:"string","timestamp without time zone":"string","timestamp with time zone":"string",interval:"string",bytea:"Buffer",uuid:"string",json:"unknown",jsonb:"unknown",inet:"string",cidr:"string",macaddr:"string",macaddr8:"string",point:"{ x: number; y: number }",line:"string",lseg:"string",box:"string",path:"string",polygon:"string",circle:"string",ARRAY:"unknown[]"};function he(a,t){let{data_type:n,udt_name:e,is_nullable:r}=a;if(t.has(e)){let u=t.get(e).map(o=>`"${o}"`).join(" | ");return r==="YES"?`(${u}) | null`:u}if(n==="ARRAY"){let c=e.replace(/^_/,"");if(t.has(c)){let s=t.get(c).map(f=>`"${f}"`).join(" | ");return r==="YES"?`(${s})[] | null`:`(${s})[]`}let u=U[c]||"unknown";return r==="YES"?`${u}[] | null`:`${u}[]`}let i=U[n]||U[e]||"unknown";return r==="YES"&&(i=`${i} | null`),i}function D(a){return a.split(/[_\-\s]+/).map(t=>t.charAt(0).toUpperCase()+t.slice(1).toLowerCase()).join("")}function ve(a,t){let n=D(a),e=t.map(r=>` | "${r}"`).join(`
57
- `);return `export type ${n} =
58
- ${e};`}function ye(a,t,n){let e=D(a),r=[],i=[],c=[];for(let f of t){let h=he(f,n),d=f.column_name,m=f.column_default!==null||f.is_identity==="YES",I=f.is_nullable==="YES";r.push(` ${d}: ${h};`),m||f.column_name==="id"?i.push(` ${d}?: ${h.replace(" | null","")} | null;`):I?i.push(` ${d}?: ${h};`):i.push(` ${d}: ${h.replace(" | null","")};`),c.push(` ${d}?: ${h.replace(" | null","")} | null;`);}let u=`export interface ${e} {
59
- ${r.join(`
56
+ `,[e]),s=new Map;for(let r of a.rows)s.set(r.table_name,[]);for(let r of t.rows){let c=s.get(r.table_name);c&&c.push(r);}let l=new Map;for(let r of i.rows){let c=l.get(r.enum_name)||[];c.push(r.enum_value),l.set(r.enum_name,c);}return {tables:s,enums:l,foreignKeys:o.rows}}var M={smallint:"number",integer:"number",bigint:"string",int2:"number",int4:"number",int8:"string",decimal:"string",numeric:"string",real:"number",float4:"number",float8:"number","double precision":"number",money:"string",boolean:"boolean",bool:"boolean",text:"string",varchar:"string",char:"string",character:"string","character varying":"string",name:"string",citext:"string",date:"string",time:"string",timetz:"string","time without time zone":"string","time with time zone":"string",timestamp:"string",timestamptz:"string","timestamp without time zone":"string","timestamp with time zone":"string",interval:"string",bytea:"Buffer",uuid:"string",json:"unknown",jsonb:"unknown",inet:"string",cidr:"string",macaddr:"string",macaddr8:"string",point:"{ x: number; y: number }",line:"string",lseg:"string",box:"string",path:"string",polygon:"string",circle:"string",ARRAY:"unknown[]"};function Te(n,e){let{data_type:a,udt_name:t,is_nullable:o}=n;if(e.has(t)){let l=e.get(t).map(r=>`"${r}"`).join(" | ");return o==="YES"?`(${l}) | null`:l}if(a==="ARRAY"){let s=t.replace(/^_/,"");if(e.has(s)){let c=e.get(s).map(h=>`"${h}"`).join(" | ");return o==="YES"?`(${c})[] | null`:`(${c})[]`}let l=M[s]||"unknown";return o==="YES"?`${l}[] | null`:`${l}[]`}let i=M[a]||M[t]||"unknown";return o==="YES"&&(i=`${i} | null`),i}function K(n){return n.split(/[_\-\s]+/).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join("")}function Se(n,e){let a=K(n),t=e.map(o=>` | "${o}"`).join(`
57
+ `);return `export type ${a} =
58
+ ${t};`}function we(n,e,a){let t=K(n),o=[],i=[],s=[];for(let h of e){let m=Te(h,a),d=h.column_name,f=h.column_default!==null||h.is_identity==="YES",A=h.is_nullable==="YES";o.push(` ${d}: ${m};`),f||h.column_name==="id"?i.push(` ${d}?: ${m.replace(" | null","")} | null;`):A?i.push(` ${d}?: ${m};`):i.push(` ${d}: ${m.replace(" | null","")};`),s.push(` ${d}?: ${m.replace(" | null","")} | null;`);}let l=`export interface ${t} {
59
+ ${o.join(`
60
60
  `)}
61
- }`,o=`export interface ${e}Insert {
61
+ }`,r=`export interface ${t}Insert {
62
62
  ${i.join(`
63
63
  `)}
64
- }`,s=`export interface ${e}Update {
65
- ${c.join(`
64
+ }`,c=`export interface ${t}Update {
65
+ ${s.join(`
66
66
  `)}
67
- }`;return {base:u,insert:o,update:s}}function be(a,t,n){let e=["/**"," * Auto-generated TypeScript types from database schema"," * Generated by @vaiftech/cli",` * Generated at: ${new Date().toISOString()}`," * "," * DO NOT EDIT MANUALLY - changes will be overwritten"," */",""];if(t.size>0){e.push("// ============ ENUMS ============"),e.push("");for(let[i,c]of t)e.push(ve(i,c)),e.push("");}e.push("// ============ TABLES ============"),e.push("");let r=[];for(let[i,c]of a){let{base:u,insert:o,update:s}=ye(i,c,t);r.push(i),e.push(u),e.push(""),e.push(o),e.push(""),e.push(s),e.push("");}e.push("// ============ DATABASE SCHEMA ============"),e.push(""),e.push("export interface Database {");for(let i of r){let c=D(i);e.push(` ${i}: {`),e.push(` Row: ${c};`),e.push(` Insert: ${c}Insert;`),e.push(` Update: ${c}Update;`),e.push(" };");}return e.push("}"),e.push(""),e.push("export type TableName = keyof Database;"),e.push(""),e.push("// ============ HELPER TYPES ============"),e.push(""),e.push('export type Row<T extends TableName> = Database[T]["Row"];'),e.push('export type Insert<T extends TableName> = Database[T]["Insert"];'),e.push('export type Update<T extends TableName> = Database[T]["Update"];'),e.push(""),e.join(`
68
- `)}async function He(a){let t=L("Loading configuration...").start();try{let n=await q(a.config),e=a.connection||n?.database?.url||process.env.DATABASE_URL,r=e&&!e.includes("${"),i,c,u;if(r){t.text="Connecting to database...";let d=new ue.Client({connectionString:e});await d.connect(),t.text="Introspecting schema...",{tables:i,enums:c,foreignKeys:u}=await ge(d,a.schema),await d.end();}else {let d=V();(!d||!d.token)&&(t.fail("No database connection and not logged in"),console.log(l.yellow(`
69
- Either:`)),console.log(l.gray(" 1. Run `vaif login` to authenticate (no DATABASE_URL needed)")),console.log(l.gray(" 2. Set DATABASE_URL in your .env file")),console.log(l.gray(" 3. Pass --connection postgresql://user:pass@host:5432/db")),process.exit(1));let m=n?.projectId||process.env.VAIF_PROJECT_ID||d.projectId;m||(t.fail("No project ID specified"),console.log(l.yellow(`
70
- Set projectId in vaif.config.json or use VAIF_PROJECT_ID env var.`)),process.exit(1)),t.text="Introspecting schema via API...",{tables:i,enums:c,foreignKeys:u}=await fe(d.token,m);}if(i.size===0){t.warn("No tables found"),console.log(l.yellow(`
71
- Push a migration first: vaif db push`));return}t.text=`Generating types for ${i.size} tables...`;let o=be(i,c,u),s=await pe.format(o,{parser:"typescript",semi:!0,singleQuote:!1,trailingComma:"es5",printWidth:100});if(a.dryRun){t.succeed("Generated types (dry run):"),console.log(""),console.log(l.gray("\u2500".repeat(60))),console.log(s),console.log(l.gray("\u2500".repeat(60)));return}let f=k.resolve(a.output),h=k.dirname(f);b.existsSync(h)||b.mkdirSync(h,{recursive:!0}),b.writeFileSync(f,s,"utf-8"),t.succeed(`Generated types for ${i.size} tables \u2192 ${l.cyan(a.output)}`),console.log(""),console.log(l.green("Generated:")),console.log(l.gray(` Tables: ${i.size}`)),console.log(l.gray(` Enums: ${c.size}`)),console.log(""),console.log(l.gray("Import in your code:")),console.log(l.cyan(` import type { Database, Row, Insert, Update } from "${a.output.replace(/\.ts$/,"")}";`));}catch(n){t.fail("Failed to generate types"),n instanceof Error&&(console.error(l.red(`
72
- Error: ${n.message}`)),n.message.includes("ECONNREFUSED")&&console.log(l.yellow(`
73
- Make sure your database is running and accessible.`))),process.exit(1);}}var T=[{name:"database",label:"Database",description:"CRUD queries, type-safe operations"},{name:"auth",label:"Authentication",description:"login, signup, OAuth, sessions"},{name:"realtime",label:"Realtime",description:"live subscriptions, presence"},{name:"storage",label:"Storage",description:"file uploads, signed URLs"},{name:"functions",label:"Functions",description:"serverless function calls"}],W={"nextjs-fullstack":{name:"Next.js Full-Stack",description:"Next.js app with server/client VAIF client, auth middleware, and React hooks",tag:"Next.js",defaultFeatures:["database","auth"],files:[{path:"package.json",content:`{
67
+ }`;return {base:l,insert:r,update:c}}function xe(n,e,a){let t=["/**"," * Auto-generated TypeScript types from database schema"," * Generated by @vaiftech/cli",` * Generated at: ${new Date().toISOString()}`," * "," * DO NOT EDIT MANUALLY - changes will be overwritten"," */",""];if(e.size>0){t.push("// ============ ENUMS ============"),t.push("");for(let[i,s]of e)t.push(Se(i,s)),t.push("");}t.push("// ============ TABLES ============"),t.push("");let o=[];for(let[i,s]of n){let{base:l,insert:r,update:c}=we(i,s,e);o.push(i),t.push(l),t.push(""),t.push(r),t.push(""),t.push(c),t.push("");}t.push("// ============ DATABASE SCHEMA ============"),t.push(""),t.push("export interface Database {");for(let i of o){let s=K(i);t.push(` ${i}: {`),t.push(` Row: ${s};`),t.push(` Insert: ${s}Insert;`),t.push(` Update: ${s}Update;`),t.push(" };");}return t.push("}"),t.push(""),t.push("export type TableName = keyof Database;"),t.push(""),t.push("// ============ HELPER TYPES ============"),t.push(""),t.push('export type Row<T extends TableName> = Database[T]["Row"];'),t.push('export type Insert<T extends TableName> = Database[T]["Insert"];'),t.push('export type Update<T extends TableName> = Database[T]["Update"];'),t.push(""),t.join(`
68
+ `)}async function ut(n){let e=j("Loading configuration...").start();try{let a=await N(n.config),t=n.connection||a?.database?.url||process.env.DATABASE_URL,o=t&&!t.includes("${"),i,s,l;if(o){e.text="Connecting to database...";let d=new ve.Client({connectionString:t});await d.connect(),e.text="Introspecting schema...",{tables:i,enums:s,foreignKeys:l}=await Ae(d,n.schema),await d.end();}else {let d=R();(!d||!d.token)&&(e.fail("No database connection and not logged in"),console.log(u.yellow(`
69
+ Either:`)),console.log(u.gray(" 1. Run `vaif login` to authenticate (no DATABASE_URL needed)")),console.log(u.gray(" 2. Set DATABASE_URL in your .env file")),console.log(u.gray(" 3. Pass --connection postgresql://user:pass@host:5432/db")),process.exit(1));let f=a?.projectId||process.env.VAIF_PROJECT_ID||d.projectId;f||(e.fail("No project ID specified"),console.log(u.yellow(`
70
+ Set projectId in vaif.config.json or use VAIF_PROJECT_ID env var.`)),process.exit(1)),e.text="Introspecting schema via API...",{tables:i,enums:s,foreignKeys:l}=await Ee(d.token,f);}if(i.size===0){e.warn("No tables found"),console.log(u.yellow(`
71
+ Push a migration first: vaif db push`));return}e.text=`Generating types for ${i.size} tables...`;let r=xe(i,s,l),c=await Ie.format(r,{parser:"typescript",semi:!0,singleQuote:!1,trailingComma:"es5",printWidth:100});if(n.dryRun){e.succeed("Generated types (dry run):"),console.log(""),console.log(u.gray("\u2500".repeat(60))),console.log(c),console.log(u.gray("\u2500".repeat(60)));return}let h=I.resolve(n.output),m=I.dirname(h);v.existsSync(m)||v.mkdirSync(m,{recursive:!0}),v.writeFileSync(h,c,"utf-8"),e.succeed(`Generated types for ${i.size} tables \u2192 ${u.cyan(n.output)}`),console.log(""),console.log(u.green("Generated:")),console.log(u.gray(` Tables: ${i.size}`)),console.log(u.gray(` Enums: ${s.size}`)),console.log(""),console.log(u.gray("Import in your code:")),console.log(u.cyan(` import type { Database, Row, Insert, Update } from "${n.output.replace(/\.ts$/,"")}";`));}catch(a){e.fail("Failed to generate types"),a instanceof Error&&(console.error(u.red(`
72
+ Error: ${a.message}`)),a.message.includes("ECONNREFUSED")&&console.log(u.yellow(`
73
+ Make sure your database is running and accessible.`))),process.exit(1);}}var C=[{name:"database",label:"Database",description:"CRUD queries, type-safe operations"},{name:"auth",label:"Authentication",description:"login, signup, OAuth, sessions"},{name:"realtime",label:"Realtime",description:"live subscriptions, presence"},{name:"storage",label:"Storage",description:"file uploads, signed URLs"},{name:"functions",label:"Functions",description:"serverless function calls"}],ee={"nextjs-fullstack":{name:"Next.js Full-Stack",description:"Next.js app with server/client VAIF client, auth middleware, and React hooks",tag:"Next.js",defaultFeatures:["database","auth"],files:[{path:"package.json",content:`{
74
74
  "name": "my-vaif-app",
75
75
  "private": true,
76
76
  "version": "0.1.0",
@@ -3774,16 +3774,1356 @@ export const posts = pgTable("posts", {
3774
3774
 
3775
3775
  return Response.json({ message: \`Hello, \${name}!\` });
3776
3776
  }
3777
- `}]},dependencies:["@vaiftech/client","@vaiftech/auth"],postInstructions:["Copy .env.example to .env.local and fill in your project credentials","Create a 'product-images' storage bucket in your VAIF dashboard","Import storage helpers from '@/lib/storage' in your API routes","Use uploadProductImage() in your product creation flow","Run: npx vaif generate to generate TypeScript types"]}};function tt(){console.log(""),console.log(l.bold("Available project templates")),console.log("");let a=26,t=22;console.log(` ${l.gray("Template".padEnd(a))}${l.gray("Stack".padEnd(t))}${l.gray("Description")}`),console.log(l.gray(" "+"-".repeat(a+t+40)));for(let[n,e]of Object.entries(W))console.log(` ${l.cyan(n.padEnd(a))}${l.yellow(e.tag.padEnd(t))}${l.white(e.description)}`);console.log(""),console.log(l.gray("Usage:")),console.log(l.gray(" npx @vaiftech/cli init --template <name>")),console.log(l.gray(" npx @vaiftech/cli init -t nextjs-fullstack")),console.log(l.gray(" npx @vaiftech/cli init -t react-spa --features auth,database,realtime")),console.log(""),console.log(l.gray("Available features: auth, database, realtime, storage, functions")),console.log("");}async function Ie(a){if(!process.stdin.isTTY||!process.stdout.isTTY)return a;let t=new Set(a.map(e=>T.findIndex(r=>r.name===e)).filter(e=>e>=0)),n=0;return new Promise(e=>{let r=$.createInterface({input:process.stdin,output:process.stdout});$.emitKeypressEvents(process.stdin,r),process.stdin.setRawMode&&process.stdin.setRawMode(true);function i(){let u=T.length+2;process.stdout.write(`\x1B[${u}A`),c();}function c(){console.log(l.bold(`
3778
- ? Which VAIF features do you want to include?`)),T.forEach((u,o)=>{let s=t.has(o)?l.green("[x]"):"[ ]",f=o===n?l.cyan("> "):" ";console.log(`${f}${s} ${u.label} ${l.gray(`(${u.description})`)}`);}),console.log(l.gray(" (up/down to move, space to toggle, enter to confirm)"));}c(),process.stdin.on("keypress",(u,o)=>{if(o.name==="up"&&n>0)n--,i();else if(o.name==="down"&&n<T.length-1)n++,i();else if(o.name==="space")t.has(n)?t.delete(n):t.add(n),i();else if(o.name==="return"){process.stdin.setRawMode&&process.stdin.setRawMode(false),r.close();let s=[...t].sort().map(f=>T[f].name);e(s.length>0?s:a);}else o.name==="c"&&o.ctrl&&(process.stdin.setRawMode&&process.stdin.setRawMode(false),r.close(),process.exit(0));});})}async function j(a,t={}){let n=W[a];n||(console.log(l.red(`
3779
- Unknown template: ${a}`)),console.log(l.yellow(`Run 'vaif templates' to see available templates.
3780
- `)),process.exit(1));let e;t.features&&t.features.length>0?e=t.features.filter(d=>T.some(m=>m.name===d)):t.addOnly?(console.log(l.red(`
3781
- No features specified.`)),console.log(l.yellow("Usage: vaif init --template <name> --add-features <features>")),console.log(l.gray("Available features: auth, database, realtime, storage, functions")),process.exit(1)):n.featureFiles&&Object.keys(n.featureFiles).length>0?e=await Ie(n.defaultFeatures??["database","auth"]):e=n.defaultFeatures??[],t.addOnly?(console.log(""),console.log(l.bold(`Adding features to ${l.cyan(n.name)} project...`)),console.log(l.gray(` Features: ${e.join(", ")}`)),console.log("")):(console.log(""),console.log(l.bold(`Scaffolding ${l.cyan(n.name)} template...`)),e.length>0&&console.log(l.gray(` Features: ${e.join(", ")}`)),console.log(""));let r=t.addOnly?[]:[...n.files],i=new Set,c=[];if(n.featureFiles)for(let d of e){let m=n.featureFiles[d];if(m)for(let I of m)i.add(I.path),c.push(I);}let u=r.filter(d=>!i.has(d.path)).concat(c),o=0,s=0;for(let d of u){let m=k.resolve(d.path),I=k.dirname(m);if(b.existsSync(I)||b.mkdirSync(I,{recursive:true}),d.path==="package.json"&&b.existsSync(m)&&!t.force)try{let y=JSON.parse(b.readFileSync(m,"utf-8")),E=JSON.parse(d.content),S=M=>{if(!M)return {};let K={};for(let[X,w]of Object.entries(M))!w.startsWith("workspace:")&&!w.startsWith("link:")&&!w.startsWith("file:")&&(K[X]=w);return K};y.dependencies={...S(y.dependencies),...E.dependencies||{}},y.devDependencies={...S(y.devDependencies),...E.devDependencies||{}},E.scripts&&(y.scripts={...y.scripts||{},...E.scripts}),b.writeFileSync(m,JSON.stringify(y,null,2)+`
3782
- `,"utf-8"),console.log(l.green(` merge ${d.path} (added dependencies)`)),o++;continue}catch{}if(b.existsSync(m)&&!t.force){console.log(l.yellow(` skip ${d.path} (already exists)`)),s++;continue}b.writeFileSync(m,d.content,"utf-8"),console.log(l.green(` create ${d.path}`)),o++;}console.log(""),o>0&&console.log(l.green(`Created ${o} file${o!==1?"s":""}.`)),s>0&&console.log(l.yellow(`Skipped ${s} file${s!==1?"s":""} (use --force to overwrite).`));let f={auth:{"@vaiftech/auth":"^1.0.0"},database:{},realtime:{},storage:{},functions:{}},h=k.resolve("package.json");if(b.existsSync(h)&&e.length>0)try{let d=JSON.parse(b.readFileSync(h,"utf-8")),m=!1;for(let I of e){let y=f[I];if(y)for(let[E,S]of Object.entries(y))d.dependencies?.[E]||(d.dependencies=d.dependencies||{},d.dependencies[E]=S,m=!0);}m&&b.writeFileSync(h,JSON.stringify(d,null,2)+`
3783
- `,"utf-8");}catch{}(n.dependencies?.length||n.devDependencies?.length)&&(console.log(""),console.log(l.bold("Install dependencies:")),n.dependencies?.length&&console.log(l.cyan(` npm install ${n.dependencies.join(" ")}`)),n.devDependencies?.length&&console.log(l.cyan(` npm install -D ${n.devDependencies.join(" ")}`))),console.log(""),console.log(l.bold.green("Project scaffolded successfully!")),console.log(""),console.log(l.bold(" Next steps:")),n.postInstructions.forEach(d=>{console.log(l.gray(` ${d}`));}),console.log(""),console.log(l.gray(" Get your project credentials at https://console.vaif.studio/security/api-keys")),console.log("");}var _e={$schema:"https://vaif.studio/schemas/config.json",projectId:"",database:{url:"${DATABASE_URL}",schema:"public"},types:{output:"./src/types/database.ts"},api:{baseUrl:"https://api.vaif.studio"}};async function ct(a){if(a.addFeatures){a.template||(console.log(l.red(`
3784
- --add-features requires --template to know which template to use.`)),console.log(l.gray("Example: vaif init --template react-spa --add-features functions,storage")),process.exit(1));let e=a.addFeatures.split(",").map(r=>r.trim());await j(a.template,{force:a.force,features:e,addOnly:true});return}let t=L("Initializing VAIF configuration...").start(),n=k.resolve("vaif.config.json");b.existsSync(n)&&!a.force&&(t.fail("vaif.config.json already exists"),console.log(l.yellow(`
3785
- Use --force to overwrite existing configuration.`)),process.exit(1));try{if(b.writeFileSync(n,JSON.stringify(_e,null,2),"utf-8"),t.succeed("Created vaif.config.json"),a.template){let e=a.features?a.features.split(",").map(r=>r.trim()):void 0;await j(a.template,{force:a.force,features:e});}else {let e=k.resolve(".env.example");if(b.existsSync(e)||(b.writeFileSync(e,`# VAIF Configuration
3777
+ `}]},dependencies:["@vaiftech/client","@vaiftech/auth"],postInstructions:["Copy .env.example to .env.local and fill in your project credentials","Create a 'product-images' storage bucket in your VAIF dashboard","Import storage helpers from '@/lib/storage' in your API routes","Use uploadProductImage() in your product creation flow","Run: npx vaif generate to generate TypeScript types"]}};function vt(){console.log(""),console.log(u.bold("Available project templates")),console.log("");let n=26,e=22;console.log(` ${u.gray("Template".padEnd(n))}${u.gray("Stack".padEnd(e))}${u.gray("Description")}`),console.log(u.gray(" "+"-".repeat(n+e+40)));for(let[a,t]of Object.entries(ee))console.log(` ${u.cyan(a.padEnd(n))}${u.yellow(t.tag.padEnd(e))}${u.white(t.description)}`);console.log(""),console.log(u.gray("Usage:")),console.log(u.gray(" npx @vaiftech/cli init --template <name>")),console.log(u.gray(" npx @vaiftech/cli init -t nextjs-fullstack")),console.log(u.gray(" npx @vaiftech/cli init -t react-spa --features auth,database,realtime")),console.log(""),console.log(u.gray("Available features: auth, database, realtime, storage, functions")),console.log("");}async function Pe(n){if(!process.stdin.isTTY||!process.stdout.isTTY)return n;let e=new Set(n.map(t=>C.findIndex(o=>o.name===t)).filter(t=>t>=0)),a=0;return new Promise(t=>{let o=H.createInterface({input:process.stdin,output:process.stdout});H.emitKeypressEvents(process.stdin,o),process.stdin.setRawMode&&process.stdin.setRawMode(true);function i(){let l=C.length+2;process.stdout.write(`\x1B[${l}A`),s();}function s(){console.log(u.bold(`
3778
+ ? Which VAIF features do you want to include?`)),C.forEach((l,r)=>{let c=e.has(r)?u.green("[x]"):"[ ]",h=r===a?u.cyan("> "):" ";console.log(`${h}${c} ${l.label} ${u.gray(`(${l.description})`)}`);}),console.log(u.gray(" (up/down to move, space to toggle, enter to confirm)"));}s(),process.stdin.on("keypress",(l,r)=>{if(r.name==="up"&&a>0)a--,i();else if(r.name==="down"&&a<C.length-1)a++,i();else if(r.name==="space")e.has(a)?e.delete(a):e.add(a),i();else if(r.name==="return"){process.stdin.setRawMode&&process.stdin.setRawMode(false),o.close();let c=[...e].sort().map(h=>C[h].name);t(c.length>0?c:n);}else r.name==="c"&&r.ctrl&&(process.stdin.setRawMode&&process.stdin.setRawMode(false),o.close(),process.exit(0));});})}async function q(n,e={}){let a=ee[n];a||(console.log(u.red(`
3779
+ Unknown template: ${n}`)),console.log(u.yellow(`Run 'vaif templates' to see available templates.
3780
+ `)),process.exit(1));let t;e.features&&e.features.length>0?t=e.features.filter(d=>C.some(f=>f.name===d)):e.addOnly?(console.log(u.red(`
3781
+ No features specified.`)),console.log(u.yellow("Usage: vaif init --template <name> --add-features <features>")),console.log(u.gray("Available features: auth, database, realtime, storage, functions")),process.exit(1)):a.featureFiles&&Object.keys(a.featureFiles).length>0?t=await Pe(a.defaultFeatures??["database","auth"]):t=a.defaultFeatures??[],e.addOnly?(console.log(""),console.log(u.bold(`Adding features to ${u.cyan(a.name)} project...`)),console.log(u.gray(` Features: ${t.join(", ")}`)),console.log("")):(console.log(""),console.log(u.bold(`Scaffolding ${u.cyan(a.name)} template...`)),t.length>0&&console.log(u.gray(` Features: ${t.join(", ")}`)),console.log(""));let o=e.addOnly?[]:[...a.files],i=new Set,s=[];if(a.featureFiles)for(let d of t){let f=a.featureFiles[d];if(f)for(let A of f)i.add(A.path),s.push(A);}let l=o.filter(d=>!i.has(d.path)).concat(s),r=0,c=0;for(let d of l){let f=I.resolve(d.path),A=I.dirname(f);if(v.existsSync(A)||v.mkdirSync(A,{recursive:true}),d.path==="package.json"&&v.existsSync(f)&&!e.force)try{let _=JSON.parse(v.readFileSync(f,"utf-8")),w=JSON.parse(d.content),U=z=>{if(!z)return {};let Y={};for(let[oe,L]of Object.entries(z))!L.startsWith("workspace:")&&!L.startsWith("link:")&&!L.startsWith("file:")&&(Y[oe]=L);return Y};_.dependencies={...U(_.dependencies),...w.dependencies||{}},_.devDependencies={...U(_.devDependencies),...w.devDependencies||{}},w.scripts&&(_.scripts={..._.scripts||{},...w.scripts}),v.writeFileSync(f,JSON.stringify(_,null,2)+`
3782
+ `,"utf-8"),console.log(u.green(` merge ${d.path} (added dependencies)`)),r++;continue}catch{}if(v.existsSync(f)&&!e.force){console.log(u.yellow(` skip ${d.path} (already exists)`)),c++;continue}v.writeFileSync(f,d.content,"utf-8"),console.log(u.green(` create ${d.path}`)),r++;}console.log(""),r>0&&console.log(u.green(`Created ${r} file${r!==1?"s":""}.`)),c>0&&console.log(u.yellow(`Skipped ${c} file${c!==1?"s":""} (use --force to overwrite).`));let h={auth:{"@vaiftech/auth":"^1.0.0"},database:{},realtime:{},storage:{},functions:{}},m=I.resolve("package.json");if(v.existsSync(m)&&t.length>0)try{let d=JSON.parse(v.readFileSync(m,"utf-8")),f=!1;for(let A of t){let _=h[A];if(_)for(let[w,U]of Object.entries(_))d.dependencies?.[w]||(d.dependencies=d.dependencies||{},d.dependencies[w]=U,f=!0);}f&&v.writeFileSync(m,JSON.stringify(d,null,2)+`
3783
+ `,"utf-8");}catch{}(a.dependencies?.length||a.devDependencies?.length)&&(console.log(""),console.log(u.bold("Install dependencies:")),a.dependencies?.length&&console.log(u.cyan(` npm install ${a.dependencies.join(" ")}`)),a.devDependencies?.length&&console.log(u.cyan(` npm install -D ${a.devDependencies.join(" ")}`))),console.log(""),console.log(u.bold.green("Project scaffolded successfully!")),console.log(""),console.log(u.bold(" Next steps:")),a.postInstructions.forEach(d=>{console.log(u.gray(` ${d}`));}),console.log(""),console.log(u.gray(" Get your project credentials at https://console.vaif.studio/security/api-keys")),console.log("");}var F=process.env.VAIF_API_URL||"https://api.vaif.studio";function Fe(n){let e=H.createInterface({input:process.stdin,output:process.stdout});return new Promise(a=>{e.question(n,t=>{e.close(),a(t.trim());});})}function Re(n,e,a){return n.projectId||e?.projectId||process.env.VAIF_PROJECT_ID||a.projectId||null}async function Ue(n){let e=j("Fetching your projects...").start();try{let a=await fetch(`${F}/projects`,{headers:{Authorization:`Bearer ${n}`}});if(!a.ok)return e.fail("Could not fetch projects"),null;let t=await a.json();if(!t||t.length===0)return e.fail("No projects found. Create a project at https://vaif.studio first."),null;if(t.length===1)return e.succeed(`Found project: ${u.cyan(t[0].name)} (${t[0].id})`),t[0].id;e.succeed(`Found ${t.length} projects
3784
+ `);for(let s=0;s<t.length;s++)console.log(u.gray(` ${s+1}.`)+` ${u.white(t[s].name)} ${u.gray(`(${t[s].id})`)}`);console.log("");let o=await Fe(u.cyan(` Select a project [1-${t.length}]: `)),i=parseInt(o,10)-1;return isNaN(i)||i<0||i>=t.length?(console.log(u.red(`
3785
+ Invalid selection.`)),null):t[i].id}catch{return e.fail("Could not fetch projects"),null}}function Le(n){if(!n||n.length===0)return "*No tables found. Create tables in the VAIF Studio dashboard.*";let e="";for(let a of n){e+=`### \`${a.name}\`
3786
+
3787
+ `,e+=`| Column | Type | Nullable | Default | Constraints |
3788
+ `,e+=`|--------|------|----------|---------|-------------|
3789
+ `;for(let t of a.columns){let o=[];t.isPrimaryKey&&o.push("PK"),t.nullable||o.push("NOT NULL");let i=a.foreignKeys?.find(s=>s.column===t.name);i&&o.push(`FK \u2192 ${i.foreignTable}.${i.foreignColumn}`),e+=`| ${t.name} | ${t.type} | ${t.nullable?"yes":"no"} | ${t.defaultValue||"-"} | ${o.join(", ")||"-"} |
3790
+ `;}e+=`
3791
+ `;}return e}function Ne(n,e,a){let t=n.slice(0,3);if(t.length===0)return "";let o="";for(let i of t){let s=i.columns.filter(r=>!r.isPrimaryKey&&r.name!=="created_at"&&r.name!=="updated_at"),l=s.slice(0,3).map(r=>` ${r.name}: ${De(r)}`).join(`,
3792
+ `);o+=`### \`${i.name}\`
3793
+
3794
+ `,o+="```typescript\n",o+=`// Select all
3795
+ const ${i.name} = await vaif.from("${i.name}").select();
3796
+
3797
+ `,o+=`// Select with filter
3798
+ const filtered = await vaif.from("${i.name}").select().eq("${s[0]?.name||"id"}", value);
3799
+
3800
+ `,o+=`// Insert
3801
+ const created = await vaif.from("${i.name}").insert({
3802
+ ${l}
3803
+ });
3804
+
3805
+ `,o+=`// Update
3806
+ await vaif.from("${i.name}").update(recordId, {
3807
+ ${s[0]?.name||"name"}: newValue
3808
+ });
3809
+
3810
+ `,o+=`// Delete
3811
+ await vaif.from("${i.name}").delete(recordId);
3812
+ `,o+="```\n\n",i===t[0]&&(o+=`#### Direct REST API (fetch)
3813
+
3814
+ `,o+="```typescript\n",o+=`// GET all rows (returns { data: [...], count: N })
3815
+ `,o+=`const res = await fetch("${e}/generated/${i.name}", {
3816
+ `,o+=` headers: { "x-vaif-key": "${a}" },
3817
+ `,o+=`});
3818
+ `,o+=`const { data } = await res.json();
3819
+
3820
+ `,o+=`// GET single row (returns { data: {...} })
3821
+ `,o+=`const res2 = await fetch("${e}/generated/${i.name}/\${id}", {
3822
+ `,o+=` headers: { "x-vaif-key": "${a}" },
3823
+ `,o+=`});
3824
+ `,o+=`const { data: record } = await res2.json();
3825
+ `,o+="```\n\n");}return o}function De(n){let e=n.type.toLowerCase();return e.includes("uuid")?'"crypto.randomUUID()"':e.includes("int")||e.includes("serial")?"42":e.includes("bool")?"true":e.includes("timestamp")||e.includes("date")?'"new Date().toISOString()"':e.includes("json")?"{}":e.includes("float")||e.includes("numeric")||e.includes("decimal")||e.includes("double")?"3.14":`"example_${n.name}"`}function ke(n){let{projectId:e,apiKey:a,apiUrl:t,projectName:o,schema:i}=n,s=Le(i.tables),l=Ne(i.tables,t,a);return `# VAIF Studio Backend
3826
+
3827
+ This project uses **VAIF Studio** as its backend. Project: **${o}** (\`${e}\`).
3828
+
3829
+ ## SDK Setup
3830
+
3831
+ \`\`\`bash
3832
+ npm install @vaiftech/client
3833
+ \`\`\`
3834
+
3835
+ \`\`\`typescript
3836
+ import { createVaifClient } from "@vaiftech/client";
3837
+
3838
+ const vaif = createVaifClient({
3839
+ baseUrl: "${t}",
3840
+ projectId: "${e}",
3841
+ apiKey: "${a}",
3842
+ });
3843
+ \`\`\`
3844
+
3845
+ ## Database Schema
3846
+
3847
+ ${s}
3848
+
3849
+ ## CRUD Examples
3850
+
3851
+ ${l}
3852
+
3853
+ ## Authentication (End-User Auth)
3854
+
3855
+ VAIF provides **project-scoped** authentication for your app's end-users. All auth routes are scoped to your project ID.
3856
+
3857
+ ### API Routes
3858
+
3859
+ | Route | Method | Auth | Description |
3860
+ |-------|--------|------|-------------|
3861
+ | \`/projects/${e}/auth/signup\` | POST | None (public) | Register a new user |
3862
+ | \`/projects/${e}/auth/login\` | POST | None (public) | Login with email/password |
3863
+ | \`/projects/${e}/auth/refresh\` | POST | Cookie | Refresh access token |
3864
+
3865
+ ### Signup
3866
+
3867
+ \`\`\`typescript
3868
+ const res = await fetch("${t}/projects/${e}/auth/signup", {
3869
+ method: "POST",
3870
+ headers: { "Content-Type": "application/json" },
3871
+ body: JSON.stringify({
3872
+ email: "user@example.com",
3873
+ password: "securePassword123",
3874
+ metadata: { displayName: "Jane Doe" }, // optional
3875
+ }),
3876
+ });
3877
+ const { accessToken, expiresIn, user } = await res.json();
3878
+ // accessToken: JWT with { sub: userId, email, projectId, type: "project_user" }
3879
+ \`\`\`
3880
+
3881
+ ### Login
3882
+
3883
+ \`\`\`typescript
3884
+ const res = await fetch("${t}/projects/${e}/auth/login", {
3885
+ method: "POST",
3886
+ headers: { "Content-Type": "application/json" },
3887
+ body: JSON.stringify({
3888
+ email: "user@example.com",
3889
+ password: "securePassword123",
3890
+ }),
3891
+ });
3892
+ const { accessToken, expiresIn, user } = await res.json();
3893
+ \`\`\`
3894
+
3895
+ ### Token Refresh
3896
+
3897
+ \`\`\`typescript
3898
+ // Refresh token is stored as httpOnly cookie (project_refresh_token)
3899
+ // and sent automatically with same-origin requests
3900
+ const res = await fetch("${t}/projects/${e}/auth/refresh", {
3901
+ method: "POST",
3902
+ credentials: "include", // sends the httpOnly cookie
3903
+ });
3904
+ const { accessToken, expiresIn, user } = await res.json();
3905
+ \`\`\`
3906
+
3907
+ ### Using Auth in Your App
3908
+
3909
+ \`\`\`typescript
3910
+ // Store the access token and use it for authenticated requests
3911
+ const headers = {
3912
+ Authorization: \\\`Bearer \\\${accessToken}\\\`,
3913
+ "x-vaif-key": "${a}",
3914
+ };
3915
+
3916
+ // The JWT contains: { sub: userId, email, projectId, type: "project_user" }
3917
+ // Use this with RLS to scope data to the current user
3918
+ \`\`\`
3919
+
3920
+ > **Important**: Auth routes are at \`/projects/{projectId}/auth/*\`, NOT \`/auth/*\`. The \`/auth/*\` routes are for VAIF Studio platform accounts, not your app's end-users.
3921
+
3922
+ ## Storage
3923
+
3924
+ \`\`\`typescript
3925
+ // Upload a file
3926
+ const { url } = await vaif.storage.upload("avatars", file, {
3927
+ contentType: "image/png",
3928
+ });
3929
+
3930
+ // Download a file
3931
+ const blob = await vaif.storage.download("avatars", "photo.png");
3932
+
3933
+ // Create a signed URL (expiring)
3934
+ const { signedUrl } = await vaif.storage.createSignedUrl("avatars", "photo.png", {
3935
+ expiresIn: 3600,
3936
+ });
3937
+
3938
+ // List files in a bucket
3939
+ const files = await vaif.storage.list("avatars", { limit: 100 });
3940
+ \`\`\`
3941
+
3942
+ ## Functions
3943
+
3944
+ \`\`\`typescript
3945
+ // Invoke a serverless function
3946
+ const result = await vaif.functions.invoke("send_welcome_email", {
3947
+ body: { userId: "user_123", template: "welcome" },
3948
+ });
3949
+ \`\`\`
3950
+
3951
+ > **Function naming**: Names must be alphanumeric and underscores only (\`^[a-zA-Z0-9_]+$\`). Use \`send_email\` not \`send-email\`.
3952
+
3953
+ ## Realtime
3954
+
3955
+ \`\`\`typescript
3956
+ // Subscribe to a channel
3957
+ const channel = vaif.realtime.channel("my-channel");
3958
+
3959
+ // Listen for postgres changes
3960
+ channel.on("postgres_changes", {
3961
+ event: "INSERT",
3962
+ schema: "public",
3963
+ table: "messages",
3964
+ }, (payload) => {
3965
+ console.log("New message:", payload.new);
3966
+ });
3967
+
3968
+ // Listen for UPDATE events
3969
+ channel.on("postgres_changes", {
3970
+ event: "UPDATE",
3971
+ schema: "public",
3972
+ table: "messages",
3973
+ }, (payload) => {
3974
+ console.log("Updated:", payload.new, "was:", payload.old);
3975
+ });
3976
+
3977
+ // Listen for DELETE events
3978
+ channel.on("postgres_changes", {
3979
+ event: "DELETE",
3980
+ schema: "public",
3981
+ table: "messages",
3982
+ }, (payload) => {
3983
+ console.log("Deleted:", payload.old);
3984
+ });
3985
+
3986
+ // Subscribe to start receiving events
3987
+ channel.subscribe();
3988
+
3989
+ // Unsubscribe when done
3990
+ channel.unsubscribe();
3991
+ \`\`\`
3992
+
3993
+ ## Row-Level Security (RLS)
3994
+
3995
+ Filter data per-user by sending the \`X-VAIF-RLS\` header with your requests. The header format is \`field:value\`, comma-separated for multiple fields.
3996
+
3997
+ \`\`\`typescript
3998
+ // SDK: pass RLS context to scope queries to the current user
3999
+ const posts = await vaif.from("posts").select({
4000
+ headers: { "x-vaif-rls": \`user_id:\${currentUser.id}\` },
4001
+ });
4002
+
4003
+ // Multiple RLS fields (e.g., multi-tenant + user scoping)
4004
+ const data = await vaif.from("documents").select({
4005
+ headers: { "x-vaif-rls": \`org_id:\${orgId},user_id:\${userId}\` },
4006
+ });
4007
+ \`\`\`
4008
+
4009
+ **How it works:**
4010
+ - On **SELECT / UPDATE / DELETE**: RLS fields are added as WHERE conditions (e.g., \`WHERE user_id = $1\`)
4011
+ - On **INSERT**: RLS fields are auto-populated into the record if not already provided
4012
+ - This enables multi-tenant data isolation without database-level RLS policies
4013
+
4014
+ ## Realtime Setup
4015
+
4016
+ Enable realtime on your tables, then subscribe to live changes:
4017
+
4018
+ \`\`\`typescript
4019
+ // 1. Enable realtime on tables via API
4020
+ await fetch("${t}/realtime/install", {
4021
+ method: "POST",
4022
+ headers: {
4023
+ Authorization: \`Bearer \${token}\`,
4024
+ "Content-Type": "application/json",
4025
+ },
4026
+ body: JSON.stringify({
4027
+ projectId: "${e}",
4028
+ tables: ["messages", "notifications"],
4029
+ }),
4030
+ });
4031
+
4032
+ // 2. Subscribe to changes via SDK
4033
+ const channel = vaif.realtime.channel("chat-room");
4034
+
4035
+ channel.on("postgres_changes", {
4036
+ event: "*", // INSERT, UPDATE, DELETE, or * for all
4037
+ schema: "public",
4038
+ table: "messages",
4039
+ }, (payload) => {
4040
+ console.log("Change:", payload.eventType, payload.new);
4041
+ });
4042
+
4043
+ channel.subscribe();
4044
+
4045
+ // 3. Presence: track who's online
4046
+ channel.on("presence", { event: "sync" }, () => {
4047
+ const state = channel.presenceState();
4048
+ console.log("Online users:", Object.keys(state));
4049
+ });
4050
+ channel.track({ user_id: currentUser.id, status: "online" });
4051
+
4052
+ // 4. Broadcast: send ephemeral messages (typing indicators, cursors)
4053
+ channel.send({
4054
+ type: "broadcast",
4055
+ event: "typing",
4056
+ payload: { userId: currentUser.id },
4057
+ });
4058
+
4059
+ // Cleanup
4060
+ channel.unsubscribe();
4061
+ \`\`\`
4062
+
4063
+ ## Storage Policies
4064
+
4065
+ Control who can access storage buckets with RLS-style policies:
4066
+
4067
+ \`\`\`typescript
4068
+ // Create a policy: only the uploader can read their own files
4069
+ await fetch("${t}/storage/buckets/\${bucketId}/policies", {
4070
+ method: "POST",
4071
+ headers: {
4072
+ Authorization: \`Bearer \${token}\`,
4073
+ "Content-Type": "application/json",
4074
+ },
4075
+ body: JSON.stringify({
4076
+ name: "owner_read",
4077
+ operation: "SELECT", // SELECT | INSERT | UPDATE | DELETE | ALL
4078
+ definition: "auth.uid() = owner_id",
4079
+ }),
4080
+ });
4081
+
4082
+ // Create a policy: authenticated users can upload
4083
+ await fetch("${t}/storage/buckets/\${bucketId}/policies", {
4084
+ method: "POST",
4085
+ headers: {
4086
+ Authorization: \`Bearer \${token}\`,
4087
+ "Content-Type": "application/json",
4088
+ },
4089
+ body: JSON.stringify({
4090
+ name: "auth_insert",
4091
+ operation: "INSERT",
4092
+ definition: "auth.uid() IS NOT NULL",
4093
+ }),
4094
+ });
4095
+ \`\`\`
4096
+
4097
+ ## Edge Function Deployment
4098
+
4099
+ Create and deploy serverless functions:
4100
+
4101
+ \`\`\`typescript
4102
+ // 1. Create a function
4103
+ const fn = await fetch("${t}/functions", {
4104
+ method: "POST",
4105
+ headers: {
4106
+ Authorization: \`Bearer \${token}\`,
4107
+ "Content-Type": "application/json",
4108
+ },
4109
+ body: JSON.stringify({
4110
+ projectId: "${e}",
4111
+ name: "send_welcome_email",
4112
+ runtime: "nodejs20", // nodejs20 (default)
4113
+ entrypoint: "index.ts", // default
4114
+ timeoutMs: 10000, // 1000\u201330000ms, default 10000
4115
+ }),
4116
+ });
4117
+
4118
+ // 2. Deploy source code
4119
+ await fetch(\`${t}/functions/\${fn.id}/source\`, {
4120
+ method: "PUT",
4121
+ headers: {
4122
+ Authorization: \`Bearer \${token}\`,
4123
+ "Content-Type": "application/json",
4124
+ },
4125
+ body: JSON.stringify({
4126
+ sourceCode: \`
4127
+ export default async function handler(req, ctx) {
4128
+ const { userId } = req.body;
4129
+ // ctx.secrets contains your encrypted secrets
4130
+ const apiKey = ctx.secrets.SENDGRID_KEY;
4131
+ return { status: "sent", userId };
4132
+ }
4133
+ \`,
4134
+ }),
4135
+ });
4136
+
4137
+ // 3. Invoke the function
4138
+ const result = await vaif.functions.invoke("send_welcome_email", {
4139
+ body: { userId: "user_123" },
4140
+ });
4141
+ \`\`\`
4142
+
4143
+ ### Authenticated Context in Functions
4144
+
4145
+ Access the caller's verified identity via \\\`vaif.auth\\\`:
4146
+
4147
+ \`\`\`typescript
4148
+ export default async function handler(req) {
4149
+ const auth = vaif.auth;
4150
+ // auth.type = 'user' | 'api_key' | 'function'
4151
+ // auth.userId \u2014 User ID (for user/function types)
4152
+ // auth.email \u2014 User email (for user type)
4153
+ // auth.projectId \u2014 Always present
4154
+ // auth.scopes \u2014 API key scopes (for api_key type)
4155
+
4156
+ if (!auth || auth.type !== 'user') {
4157
+ return { statusCode: 401, body: { error: 'Unauthorized' } };
4158
+ }
4159
+
4160
+ return { body: { message: "Hello " + auth.email } };
4161
+ }
4162
+ \`\`\`
4163
+
4164
+ ### Function-to-Function Invocation
4165
+
4166
+ Call other functions from within a handler:
4167
+
4168
+ \`\`\`typescript
4169
+ export default async function handler(req) {
4170
+ const result = await vaif.invoke("send_email", {
4171
+ to: "user@example.com",
4172
+ subject: "Hello",
4173
+ });
4174
+ return { statusCode: 200, body: result };
4175
+ }
4176
+ \`\`\`
4177
+
4178
+ ### Database Triggers
4179
+
4180
+ Fire functions automatically on insert/update/delete events. Configure triggers via the API:
4181
+
4182
+ \`\`\`
4183
+ POST /functions/\\\${functionId}/triggers
4184
+ { "event": "db.insert", "tableName": "orders", "enabled": true }
4185
+ \`\`\`
4186
+
4187
+ ## API Key Management
4188
+
4189
+ API keys are project-scoped and used for data-plane authentication (CRUD, storage, functions).
4190
+
4191
+ \`\`\`typescript
4192
+ // Create a new API key
4193
+ const { key } = await fetch("${t}/projects/${e}/api-keys", {
4194
+ method: "POST",
4195
+ headers: {
4196
+ Authorization: \`Bearer \${token}\`,
4197
+ "Content-Type": "application/json",
4198
+ },
4199
+ body: JSON.stringify({ name: "production-frontend" }),
4200
+ }).then(r => r.json());
4201
+
4202
+ // List keys
4203
+ const keys = await fetch("${t}/projects/${e}/api-keys", {
4204
+ headers: { Authorization: \`Bearer \${token}\` },
4205
+ }).then(r => r.json());
4206
+
4207
+ // Rotate a key (generates new secret, old key stops working)
4208
+ await fetch(\`${t}/projects/${e}/api-keys/\${keyId}/rotate\`, {
4209
+ method: "POST",
4210
+ headers: { Authorization: \`Bearer \${token}\` },
4211
+ });
4212
+
4213
+ // Revoke a key
4214
+ await fetch(\`${t}/projects/${e}/api-keys/\${keyId}/revoke\`, {
4215
+ method: "POST",
4216
+ headers: { Authorization: \`Bearer \${token}\` },
4217
+ });
4218
+ \`\`\`
4219
+
4220
+ ## Secrets & Environment Variables
4221
+
4222
+ Secrets are encrypted at rest and injected into function invocations at runtime.
4223
+
4224
+ \`\`\`typescript
4225
+ // Set a secret (via API)
4226
+ await fetch("${t}/functions/secrets", {
4227
+ method: "POST",
4228
+ headers: {
4229
+ Authorization: \`Bearer \${token}\`,
4230
+ "Content-Type": "application/json",
4231
+ },
4232
+ body: JSON.stringify({
4233
+ projectId: "${e}",
4234
+ key: "STRIPE_SECRET_KEY",
4235
+ value: "sk_live_...",
4236
+ }),
4237
+ });
4238
+
4239
+ // Or use the CLI
4240
+ // vaif secrets set STRIPE_SECRET_KEY sk_live_...
4241
+ // vaif secrets list
4242
+ // vaif secrets delete STRIPE_SECRET_KEY
4243
+ \`\`\`
4244
+
4245
+ **Accessing secrets in functions:**
4246
+
4247
+ \`\`\`typescript
4248
+ export default async function handler(req, ctx) {
4249
+ const stripe = new Stripe(ctx.secrets.STRIPE_SECRET_KEY);
4250
+ // ...
4251
+ }
4252
+ \`\`\`
4253
+
4254
+ ## API Reference Notes
4255
+
4256
+ ### Filter Syntax
4257
+
4258
+ The SDK supports these filter operators:
4259
+
4260
+ | Operator | Description | Example |
4261
+ |----------|-------------|---------|
4262
+ | \`eq\` | Equal | \`.eq("status", "active")\` |
4263
+ | \`neq\` | Not equal | \`.neq("status", "deleted")\` |
4264
+ | \`gt\` | Greater than | \`.gt("age", 18)\` |
4265
+ | \`lt\` | Less than | \`.lt("price", 100)\` |
4266
+ | \`gte\` | Greater than or equal | \`.gte("score", 90)\` |
4267
+ | \`lte\` | Less than or equal | \`.lte("count", 10)\` |
4268
+ | \`in\` | In array | \`.in("role", ["admin", "editor"])\` |
4269
+ | \`like\` | Pattern match (case-sensitive) | \`.like("name", "%john%")\` |
4270
+ | \`ilike\` | Pattern match (case-insensitive) | \`.ilike("name", "%john%")\` |
4271
+ | \`is\` | IS comparison (null, true, false) | \`.is("deleted_at", null)\` |
4272
+
4273
+ ### JSONB Subkey Filters
4274
+
4275
+ Filter on nested JSONB fields using arrow notation:
4276
+
4277
+ | Filter | SQL Generated |
4278
+ |--------|--------------|
4279
+ | \\\`filter[metadata->status]=active\\\` | \\\`metadata->>'status' = 'active'\\\` |
4280
+ | \\\`filter[config->theme.ilike]=%dark%\\\` | \\\`config->>'theme' ILIKE '%dark%'\\\` |
4281
+ | \\\`filter[data->user->role]=admin\\\` | \\\`data->'user'->>'role' = 'admin'\\\` |
4282
+
4283
+ All standard operators work with JSONB paths. The last segment uses \\\`->>\\\` (text extraction).
4284
+
4285
+ ### Compound Filters (AND + OR)
4286
+
4287
+ Combine AND and OR conditions:
4288
+
4289
+ \`\`\`
4290
+ ?filter[status]=active&or_filter[role]=admin&or_filter[role]=moderator
4291
+ \`\`\`
4292
+
4293
+ This generates: \\\`WHERE status = 'active' AND (role = 'admin' OR role = 'moderator')\\\`
4294
+
4295
+ ### Full-Text Search
4296
+
4297
+ \`\`\`typescript
4298
+ const results = await fetch("${t}/generated/posts/search", {
4299
+ method: "POST",
4300
+ headers: { "x-vaif-key": "${a}", "Content-Type": "application/json" },
4301
+ body: JSON.stringify({
4302
+ query: "search term",
4303
+ columns: ["title", "body"],
4304
+ limit: 20,
4305
+ }),
4306
+ });
4307
+ // Results ranked by ts_rank score
4308
+ \`\`\`
4309
+
4310
+ ### Aggregation
4311
+
4312
+ \`\`\`typescript
4313
+ const stats = await fetch("${t}/generated/orders/aggregate", {
4314
+ method: "POST",
4315
+ headers: { "x-vaif-key": "${a}", "Content-Type": "application/json" },
4316
+ body: JSON.stringify({
4317
+ aggregates: [
4318
+ { fn: "count", column: "*" },
4319
+ { fn: "sum", column: "total" },
4320
+ { fn: "avg", column: "total" },
4321
+ ],
4322
+ groupBy: ["status"],
4323
+ }),
4324
+ });
4325
+ \`\`\`
4326
+
4327
+ ### Joins (Foreign Key Includes)
4328
+
4329
+ Include related rows by specifying foreign key columns:
4330
+
4331
+ \`\`\`
4332
+ GET /generated/posts?include=author_id&include=category_id
4333
+ \`\`\`
4334
+
4335
+ Returns posts with \\\`author_id_included\\\` and \\\`category_id_included\\\` objects containing the related rows.
4336
+
4337
+ ### Upsert
4338
+
4339
+ Insert or update on conflict:
4340
+
4341
+ \`\`\`typescript
4342
+ const result = await fetch("${t}/generated/users", {
4343
+ method: "POST",
4344
+ headers: { "x-vaif-key": "${a}", "Content-Type": "application/json" },
4345
+ body: JSON.stringify({
4346
+ email: "alice@example.com",
4347
+ name: "Alice",
4348
+ _upsert: true,
4349
+ _conflictColumns: ["email"],
4350
+ }),
4351
+ });
4352
+ \`\`\`
4353
+
4354
+ ### Pagination
4355
+
4356
+ \`\`\`typescript
4357
+ // Default: limit 20, offset 0
4358
+ const page1 = await vaif.from("posts").select().limit(20).offset(0);
4359
+ const page2 = await vaif.from("posts").select().limit(20).offset(20);
4360
+ \`\`\`
4361
+
4362
+ ### REST API Response Format
4363
+
4364
+ When calling the REST API directly (without the SDK), all data-plane responses are wrapped:
4365
+
4366
+ \`\`\`typescript
4367
+ // GET /generated/{table} \u2192 list
4368
+ { data: [...], count: 5 }
4369
+
4370
+ // GET /generated/{table}/{id} \u2192 single record
4371
+ { data: { id: "...", ... } }
4372
+
4373
+ // POST /generated/{table} \u2192 created record
4374
+ { data: { id: "...", ... } }
4375
+
4376
+ // PATCH /generated/{table}/{id} \u2192 updated record
4377
+ { data: { id: "...", ... } }
4378
+
4379
+ // DELETE /generated/{table}/{id}
4380
+ { ok: true }
4381
+
4382
+ // Error responses
4383
+ { error: "NotFound", message: "...", requestId: "..." }
4384
+ \`\`\`
4385
+
4386
+ The SDK unwraps these automatically, but if you use \`fetch()\` directly, access the data via \`response.data\`.
4387
+
4388
+ ### Numeric/Decimal Column Serialization
4389
+
4390
+ PostgreSQL \`numeric\` and \`decimal\` columns serialize to **JSON strings** (to preserve arbitrary precision). This is standard behavior. Any column typed as \`numeric\` or \`decimal\` will arrive as \`"3.14"\` not \`3.14\`.
4391
+
4392
+ \`\`\`typescript
4393
+ // Wrong \u2014 value is a string, comparison may fail
4394
+ if (item.price > 10.0) { ... }
4395
+
4396
+ // Correct \u2014 parse before arithmetic
4397
+ if (parseFloat(item.price) > 10.0) { ... }
4398
+ \`\`\`
4399
+
4400
+ ### Auth Headers
4401
+
4402
+ VAIF uses **two auth modes** \u2014 choose the right one for each operation:
4403
+
4404
+ | Auth Mode | Header | Used For |
4405
+ |-----------|--------|----------|
4406
+ | **API Key** | \`x-vaif-key: vaif_xxx\` | Data-plane: CRUD (\`/generated/*\`), storage uploads/downloads, function invocation |
4407
+ | **JWT Token** | \`Authorization: Bearer <jwt>\` | Control-plane: schema introspection, project management, function CRUD, bucket creation |
4408
+
4409
+ > **Important**: API keys do NOT work for control-plane endpoints (creating functions, managing buckets, schema changes). Those require a JWT session token. The MCP server handles this automatically by using both auth modes.
4410
+
4411
+ ### MCP Tools (via .mcp.json)
4412
+
4413
+ The \`.mcp.json\` file configures an MCP server that gives Claude Code direct access to your VAIF project. Available tools:
4414
+
4415
+ | Tool | What it does |
4416
+ |------|-------------|
4417
+ | \`list_tables\`, \`describe_table\` | Inspect database schema |
4418
+ | \`get_schema\` | Full schema as JSON |
4419
+ | \`create_tables\` | Create or update tables declaratively |
4420
+ | \`query_rows\` | Query with filters, JSONB paths, pagination |
4421
+ | \`insert_row\`, \`update_row\`, \`delete_row\` | CRUD operations on any table |
4422
+ | \`list_functions\`, \`deploy_function\`, \`invoke_function\` | Function management |
4423
+ | \`get_function_logs\` | Execution history with status filters |
4424
+ | \`set_secret\`, \`list_secrets\`, \`delete_secret\` | Function secrets |
4425
+ | \`list_buckets\`, \`list_files\`, \`get_signed_url\` | Storage operations |
4426
+ | \`enable_realtime\`, \`realtime_status\` | Realtime subscriptions |
4427
+
4428
+ > **Note**: MCP tools are only available to the main Claude Code session, not to spawned sub-agents (Task tool). The main session should handle all VAIF backend operations directly.
4429
+ `}async function ne(n){let e=j(),a=R();(!a||!a.token)&&(console.log(u.red("Not logged in")),console.log(u.gray("Run `vaif login` first to authenticate")),process.exit(1));let t=n.config||"vaif.config.json",o=null;try{o=await N(t);}catch{}let i=Re(n,o,a);i||(console.log(u.yellow(`No project ID specified \u2014 fetching your projects...
4430
+ `)),i=await Ue(a.token),i||(console.log(u.gray(`
4431
+ Tip: pass --project-id <id> or set projectId in vaif.config.json`)),process.exit(1)));let s=I.resolve(n.outputDir||".");console.log(""),console.log(u.bold("VAIF Claude Code Setup")),console.log(u.gray(` Project: ${i}`)),console.log(""),e.start("Fetching database schema...");let l={tables:[]};try{let m=await fetch(`${F}/schema-engine/introspect/${i}`,{headers:{Authorization:`Bearer ${a.token}`}});m.ok?(l=await m.json(),e.succeed(`Fetched schema (${l.tables?.length||0} tables)`)):e.warn("Could not fetch schema \u2014 continuing without it");}catch{e.warn("Could not fetch schema \u2014 continuing without it");}e.start("Fetching project info...");let r=i,c=F;try{let m=await fetch(`${F}/projects/${i}`,{headers:{Authorization:`Bearer ${a.token}`}});if(m.ok){let d=await m.json(),f=d.project||d;r=f.name||i,c=f.apiUrl||F,e.succeed(`Project: ${r}`);}else e.warn("Could not fetch project info \u2014 using defaults");}catch{e.warn("Could not fetch project info \u2014 using defaults");}let h=n.apiKey||o?.api?.apiKey||"";if(!h){e.start("Generating API key for Claude Code...");try{let m=`claude-code-${Date.now()}`,d=await fetch(`${F}/projects/${i}/api-keys`,{method:"POST",headers:{Authorization:`Bearer ${a.token}`,"Content-Type":"application/json"},body:JSON.stringify({name:m,scopes:["crud","realtime","functions","storage"]})});if(d.ok){let f=await d.json();h=f.apiKey||f.key,e.succeed(`Generated API key: ${m}`);}else e.fail("Could not auto-generate API key"),console.log(u.yellow(" Pass one with --api-key or generate via `vaif keys generate`")),process.exit(1);}catch{e.fail("Could not auto-generate API key"),console.log(u.yellow(" Pass one with --api-key or generate via `vaif keys generate`")),process.exit(1);}}if(!n.skipMcp){e.start("Writing .mcp.json...");let m={mcpServers:{"vaif-studio":{command:"npx",args:["@vaiftech/mcp"],env:{VAIF_API_KEY:h,VAIF_PROJECT_ID:i,VAIF_API_URL:c,VAIF_AUTH_TOKEN:a.token}}}},d=I.join(s,".mcp.json");v.writeFileSync(d,JSON.stringify(m,null,2)+`
4432
+ `,"utf-8"),e.succeed(`Written ${u.cyan(".mcp.json")}`);}if(!n.skipClaudeMd){e.start("Writing CLAUDE.md...");let m=ke({projectId:i,apiKey:h,apiUrl:c,projectName:r,schema:l}),d=I.join(s,"CLAUDE.md");v.writeFileSync(d,m,"utf-8"),e.succeed(`Written ${u.cyan("CLAUDE.md")}`);}console.log(""),console.log(u.green.bold(" Claude Code integration configured!")),console.log(""),n.skipMcp||console.log(u.gray(" MCP Server: ")+u.white(".mcp.json")),n.skipClaudeMd||console.log(u.gray(" Context: ")+u.white("CLAUDE.md")),console.log(""),console.log(u.bold(" Next steps:")),console.log(u.gray(" 1. Open this project in Claude Code")),console.log(u.gray(" 2. The MCP server auto-connects to your VAIF project")),console.log(u.gray(" 3. Ask Claude to query, modify, or build against your schema")),console.log("");}var k={base:`# VAIF Studio Backend
4433
+
4434
+ This project uses **VAIF Studio** as its backend.
4435
+
4436
+ ## SDK Setup
4437
+
4438
+ \`\`\`bash
4439
+ npm install @vaiftech/client
4440
+ \`\`\`
4441
+
4442
+ \`\`\`typescript
4443
+ import { createVaifClient } from "@vaiftech/client";
4444
+
4445
+ const vaif = createVaifClient({
4446
+ baseUrl: "https://api.vaif.studio",
4447
+ projectId: process.env.VAIF_PROJECT_ID!,
4448
+ apiKey: process.env.VAIF_API_KEY!,
4449
+ });
4450
+ \`\`\`
4451
+
4452
+ ## MCP Server
4453
+
4454
+ This project includes an MCP server for Claude Code integration. Tools available:
4455
+
4456
+ - **Database**: list_tables, describe_table, query_rows, insert_row, update_row, delete_row
4457
+ - **Schema**: get_schema, create_tables
4458
+ - **Storage**: list_buckets, create_bucket, list_files, get_signed_url
4459
+ - **Functions**: list_functions, invoke_function, create_function, deploy_function
4460
+ - **Auth**: list_api_keys, create_api_key
4461
+ - **Realtime**: realtime_status, enable_realtime
4462
+
4463
+ ## Database
4464
+
4465
+ \`\`\`typescript
4466
+ // Query
4467
+ const { data } = await vaif.from("table_name").select().eq("column", value);
4468
+
4469
+ // Insert
4470
+ const { data } = await vaif.from("table_name").insert({ column: value });
4471
+
4472
+ // Update
4473
+ await vaif.from("table_name").update({ column: newValue }).eq("id", recordId);
4474
+
4475
+ // Delete
4476
+ await vaif.from("table_name").delete().eq("id", recordId);
4477
+ \`\`\`
4478
+
4479
+ ## Authentication
4480
+
4481
+ \`\`\`typescript
4482
+ // Sign up
4483
+ await vaif.auth.signUp({ email, password });
4484
+
4485
+ // Sign in
4486
+ const { data } = await vaif.auth.signIn({ email, password });
4487
+
4488
+ // OAuth
4489
+ await vaif.auth.signInWithOAuth({ provider: "google", redirectTo: callbackUrl });
4490
+
4491
+ // Get current user
4492
+ const user = await vaif.auth.getUser();
4493
+ \`\`\`
4494
+
4495
+ ## Storage
4496
+
4497
+ \`\`\`typescript
4498
+ // Upload
4499
+ await vaif.storage.from("bucket").upload("path/file.png", file);
4500
+
4501
+ // Signed URL
4502
+ const { url } = await vaif.storage.from("bucket").createSignedUrl("path/file.png", 3600);
4503
+
4504
+ // List files
4505
+ const { data } = await vaif.storage.from("bucket").list("path/");
4506
+ \`\`\`
4507
+
4508
+ ## Functions
4509
+
4510
+ \`\`\`typescript
4511
+ // Invoke
4512
+ const result = await vaif.functions.invoke("function_name", { body: { key: "value" } });
4513
+ \`\`\`
4514
+
4515
+ > Function names must be alphanumeric + underscores only (\`^[a-zA-Z0-9_]+$\`).
4516
+
4517
+ ## Realtime
4518
+
4519
+ \`\`\`typescript
4520
+ const subscription = vaif.realtime
4521
+ .channel("table_name")
4522
+ .on("INSERT", (payload) => console.log("New:", payload.new))
4523
+ .on("UPDATE", (payload) => console.log("Updated:", payload.new))
4524
+ .on("DELETE", (payload) => console.log("Removed:", payload.old))
4525
+ .subscribe();
4526
+ \`\`\`
4527
+
4528
+ ## Filter Operators
4529
+
4530
+ | Operator | Description | Example |
4531
+ |----------|-------------|---------|
4532
+ | eq | Equal | \`.eq("status", "active")\` |
4533
+ | neq | Not equal | \`.neq("status", "deleted")\` |
4534
+ | gt / gte | Greater than | \`.gt("age", 18)\` |
4535
+ | lt / lte | Less than | \`.lt("price", 100)\` |
4536
+ | in | In array | \`.in("role", ["admin", "editor"])\` |
4537
+ | like / ilike | Pattern match | \`.ilike("name", "%john%")\` |
4538
+ | is | IS NULL check | \`.is("deleted_at", null)\` |
4539
+
4540
+ ## Auth Headers
4541
+
4542
+ | Mode | Header | Used For |
4543
+ |------|--------|----------|
4544
+ | API Key | \`x-vaif-key: vk_xxx\` | Data-plane: CRUD, storage, functions |
4545
+ | JWT Token | \`Authorization: Bearer <jwt>\` | Control-plane: schema, project management |
4546
+
4547
+ ## Environment Variables
4548
+
4549
+ \`\`\`bash
4550
+ VAIF_API_URL=https://api.vaif.studio
4551
+ VAIF_PROJECT_ID=proj_xxx
4552
+ VAIF_API_KEY=vk_xxx
4553
+ \`\`\`
4554
+ `,saas:`# VAIF Studio \u2014 SaaS Backend
4555
+
4556
+ This project uses **VAIF Studio** as its backend for a SaaS application.
4557
+
4558
+ ## SDK Setup
4559
+
4560
+ \`\`\`bash
4561
+ npm install @vaiftech/client @vaiftech/react
4562
+ \`\`\`
4563
+
4564
+ \`\`\`typescript
4565
+ import { createVaifClient } from "@vaiftech/client";
4566
+
4567
+ const vaif = createVaifClient({
4568
+ baseUrl: "https://api.vaif.studio",
4569
+ projectId: process.env.VAIF_PROJECT_ID!,
4570
+ apiKey: process.env.VAIF_API_KEY!,
4571
+ });
4572
+ \`\`\`
4573
+
4574
+ ## Common SaaS Schema Patterns
4575
+
4576
+ ### Multi-Tenant with Organizations
4577
+
4578
+ \`\`\`sql
4579
+ -- Organizations (tenants)
4580
+ CREATE TABLE orgs (
4581
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4582
+ name TEXT NOT NULL,
4583
+ slug TEXT UNIQUE NOT NULL,
4584
+ plan TEXT DEFAULT 'free',
4585
+ created_at TIMESTAMPTZ DEFAULT now()
4586
+ );
4587
+
4588
+ -- Organization members
4589
+ CREATE TABLE org_members (
4590
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4591
+ org_id UUID REFERENCES orgs(id) ON DELETE CASCADE,
4592
+ user_id UUID NOT NULL,
4593
+ role TEXT DEFAULT 'member', -- owner, admin, member
4594
+ created_at TIMESTAMPTZ DEFAULT now(),
4595
+ UNIQUE(org_id, user_id)
4596
+ );
4597
+
4598
+ -- Tenant-scoped data
4599
+ CREATE TABLE projects (
4600
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4601
+ org_id UUID REFERENCES orgs(id) ON DELETE CASCADE,
4602
+ name TEXT NOT NULL,
4603
+ settings JSONB DEFAULT '{}',
4604
+ created_at TIMESTAMPTZ DEFAULT now()
4605
+ );
4606
+ CREATE INDEX idx_projects_org ON projects(org_id);
4607
+ \`\`\`
4608
+
4609
+ ### Row-Level Security for Multi-Tenancy
4610
+
4611
+ \`\`\`typescript
4612
+ // Scope all queries to the current organization
4613
+ const projects = await vaif.from("projects").select().eq("org_id", currentOrgId);
4614
+
4615
+ // Or use RLS headers for automatic scoping
4616
+ const data = await vaif.from("projects").select({
4617
+ headers: { "x-vaif-rls": \\\`org_id:\${currentOrgId}\\\` },
4618
+ });
4619
+ \`\`\`
4620
+
4621
+ ### Auth Flows
4622
+
4623
+ \`\`\`typescript
4624
+ // Sign up \u2192 create org \u2192 add as owner
4625
+ const { data: user } = await vaif.auth.signUp({ email, password });
4626
+ const { data: org } = await vaif.from("orgs").insert({ name: orgName, slug });
4627
+ await vaif.from("org_members").insert({ org_id: org.id, user_id: user.id, role: "owner" });
4628
+ \`\`\`
4629
+
4630
+ ### Billing Integration
4631
+
4632
+ \`\`\`typescript
4633
+ // Store Stripe customer/subscription IDs
4634
+ CREATE TABLE billing_accounts (
4635
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4636
+ org_id UUID UNIQUE REFERENCES orgs(id),
4637
+ stripe_customer_id TEXT,
4638
+ stripe_subscription_id TEXT,
4639
+ plan TEXT DEFAULT 'free',
4640
+ period_start TIMESTAMPTZ,
4641
+ period_end TIMESTAMPTZ
4642
+ );
4643
+
4644
+ // Webhook handler (VAIF Function)
4645
+ export default async function handler(req, ctx) {
4646
+ const event = req.body;
4647
+ if (event.type === "customer.subscription.updated") {
4648
+ const sub = event.data.object;
4649
+ await vaif.from("billing_accounts")
4650
+ .update({ plan: sub.metadata.plan, period_end: sub.current_period_end })
4651
+ .eq("stripe_subscription_id", sub.id);
4652
+ }
4653
+ return { received: true };
4654
+ }
4655
+ \`\`\`
4656
+
4657
+ ### Invite System
4658
+
4659
+ \`\`\`sql
4660
+ CREATE TABLE invites (
4661
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4662
+ org_id UUID REFERENCES orgs(id) ON DELETE CASCADE,
4663
+ email TEXT NOT NULL,
4664
+ role TEXT DEFAULT 'member',
4665
+ token TEXT UNIQUE NOT NULL,
4666
+ expires_at TIMESTAMPTZ NOT NULL,
4667
+ accepted_at TIMESTAMPTZ
4668
+ );
4669
+ \`\`\`
4670
+
4671
+ ## API Key Scoping
4672
+
4673
+ \`\`\`
4674
+ x-vaif-key: vk_xxx \u2192 Data-plane (CRUD, storage, functions)
4675
+ Authorization: Bearer <jwt> \u2192 Control-plane (schema, project management)
4676
+ \`\`\`
4677
+
4678
+ ## Environment Variables
4679
+
4680
+ \`\`\`bash
4681
+ VAIF_API_URL=https://api.vaif.studio
4682
+ VAIF_PROJECT_ID=proj_xxx
4683
+ VAIF_API_KEY=vk_xxx
4684
+ STRIPE_SECRET_KEY=sk_live_xxx
4685
+ STRIPE_WEBHOOK_SECRET=whsec_xxx
4686
+ \`\`\`
4687
+ `,mobile:`# VAIF Studio \u2014 Mobile App Backend
4688
+
4689
+ This project uses **VAIF Studio** as its backend for a mobile application (React Native/Expo, Flutter, or Swift).
4690
+
4691
+ ## SDK Setup
4692
+
4693
+ ### React Native / Expo
4694
+ \`\`\`bash
4695
+ npm install @vaiftech/sdk-expo
4696
+ \`\`\`
4697
+
4698
+ \`\`\`typescript
4699
+ import { VaifProvider, useVaif } from "@vaiftech/sdk-expo";
4700
+
4701
+ export default function App() {
4702
+ return (
4703
+ <VaifProvider
4704
+ config={{
4705
+ baseUrl: "https://api.vaif.studio",
4706
+ projectId: "proj_xxx",
4707
+ apiKey: "vk_xxx",
4708
+ }}
4709
+ >
4710
+ <MainApp />
4711
+ </VaifProvider>
4712
+ );
4713
+ }
4714
+ \`\`\`
4715
+
4716
+ ### Flutter / Dart
4717
+ \`\`\`yaml
4718
+ # pubspec.yaml
4719
+ dependencies:
4720
+ vaif_client: ^1.0.0
4721
+ \`\`\`
4722
+
4723
+ \`\`\`dart
4724
+ import 'package:vaif_client/vaif_client.dart';
4725
+
4726
+ final vaif = VaifClient(
4727
+ baseUrl: 'https://api.vaif.studio',
4728
+ projectId: 'proj_xxx',
4729
+ apiKey: 'vk_xxx',
4730
+ );
4731
+ \`\`\`
4732
+
4733
+ ### Swift / iOS
4734
+ \`\`\`swift
4735
+ // Package.swift
4736
+ .package(url: "https://github.com/vaifllc/vaif-swift", from: "0.2.0")
4737
+
4738
+ import VaifClient
4739
+
4740
+ let vaif = VaifClient(
4741
+ baseUrl: "https://api.vaif.studio",
4742
+ projectId: "proj_xxx",
4743
+ apiKey: "vk_xxx"
4744
+ )
4745
+ \`\`\`
4746
+
4747
+ ## Common Mobile Patterns
4748
+
4749
+ ### User Profiles with Avatars
4750
+
4751
+ \`\`\`sql
4752
+ CREATE TABLE profiles (
4753
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4754
+ user_id UUID UNIQUE NOT NULL,
4755
+ display_name TEXT,
4756
+ avatar_url TEXT,
4757
+ bio TEXT,
4758
+ push_token TEXT,
4759
+ device_type TEXT, -- ios, android
4760
+ last_seen TIMESTAMPTZ DEFAULT now()
4761
+ );
4762
+ \`\`\`
4763
+
4764
+ ### Push Notifications
4765
+
4766
+ \`\`\`typescript
4767
+ // Store push tokens
4768
+ await vaif.from("profiles").update({
4769
+ push_token: expoPushToken,
4770
+ device_type: Platform.OS,
4771
+ }).eq("user_id", userId);
4772
+
4773
+ // Send notification (VAIF Function)
4774
+ export default async function handler(req, ctx) {
4775
+ const { userId, title, body } = req.body;
4776
+ const { data: profile } = await vaif.from("profiles")
4777
+ .select("push_token, device_type")
4778
+ .eq("user_id", userId)
4779
+ .single();
4780
+
4781
+ // Send via Expo Push API or APNs/FCM
4782
+ await fetch("https://exp.host/--/api/v2/push/send", {
4783
+ method: "POST",
4784
+ headers: { "Content-Type": "application/json" },
4785
+ body: JSON.stringify({
4786
+ to: profile.push_token,
4787
+ title, body,
4788
+ }),
4789
+ });
4790
+ return { sent: true };
4791
+ }
4792
+ \`\`\`
4793
+
4794
+ ### Offline-First with Sync
4795
+
4796
+ \`\`\`typescript
4797
+ // Cache data locally, sync when online
4798
+ import AsyncStorage from "@react-native-async-storage/async-storage";
4799
+
4800
+ // Fetch and cache
4801
+ const { data } = await vaif.from("items").select();
4802
+ await AsyncStorage.setItem("items_cache", JSON.stringify(data));
4803
+
4804
+ // Read from cache when offline
4805
+ const cached = JSON.parse(await AsyncStorage.getItem("items_cache") || "[]");
4806
+ \`\`\`
4807
+
4808
+ ### Image Upload from Camera
4809
+
4810
+ \`\`\`typescript
4811
+ import * as ImagePicker from "expo-image-picker";
4812
+
4813
+ const result = await ImagePicker.launchCameraAsync({ quality: 0.8 });
4814
+ if (!result.canceled) {
4815
+ const uri = result.assets[0].uri;
4816
+ const response = await fetch(uri);
4817
+ const blob = await response.blob();
4818
+
4819
+ await vaif.storage.from("avatars").upload(
4820
+ \\\`\${userId}/avatar.jpg\\\`,
4821
+ blob,
4822
+ { contentType: "image/jpeg" }
4823
+ );
4824
+ }
4825
+ \`\`\`
4826
+
4827
+ ### Realtime Chat
4828
+
4829
+ \`\`\`typescript
4830
+ // Subscribe to new messages
4831
+ const subscription = vaif.realtime
4832
+ .channel("messages")
4833
+ .on("INSERT", (payload) => {
4834
+ setMessages(prev => [...prev, payload.new]);
4835
+ })
4836
+ .subscribe();
4837
+
4838
+ // Send a message
4839
+ await vaif.from("messages").insert({
4840
+ room_id: roomId,
4841
+ user_id: userId,
4842
+ content: messageText,
4843
+ });
4844
+ \`\`\`
4845
+
4846
+ ## Auth with Secure Token Storage
4847
+
4848
+ The Expo SDK automatically stores auth tokens in SecureStore (iOS Keychain / Android Keystore).
4849
+
4850
+ \`\`\`typescript
4851
+ const { useAuth } = require("@vaiftech/sdk-expo");
4852
+
4853
+ function LoginScreen() {
4854
+ const { signIn, signUp, user, loading } = useAuth();
4855
+
4856
+ const handleLogin = async () => {
4857
+ const { error } = await signIn({ email, password });
4858
+ if (error) Alert.alert("Error", error.message);
4859
+ };
4860
+ }
4861
+ \`\`\`
4862
+
4863
+ ## Environment Variables
4864
+
4865
+ \`\`\`bash
4866
+ VAIF_API_URL=https://api.vaif.studio
4867
+ VAIF_PROJECT_ID=proj_xxx
4868
+ VAIF_API_KEY=vk_xxx
4869
+ EXPO_PUBLIC_VAIF_PROJECT_ID=proj_xxx
4870
+ EXPO_PUBLIC_VAIF_API_KEY=vk_xxx
4871
+ \`\`\`
4872
+ `,ecommerce:`# VAIF Studio \u2014 E-Commerce Backend
4873
+
4874
+ This project uses **VAIF Studio** as its backend for an e-commerce application.
4875
+
4876
+ ## SDK Setup
4877
+
4878
+ \`\`\`bash
4879
+ npm install @vaiftech/client
4880
+ \`\`\`
4881
+
4882
+ \`\`\`typescript
4883
+ import { createVaifClient } from "@vaiftech/client";
4884
+
4885
+ const vaif = createVaifClient({
4886
+ baseUrl: "https://api.vaif.studio",
4887
+ projectId: process.env.VAIF_PROJECT_ID!,
4888
+ apiKey: process.env.VAIF_API_KEY!,
4889
+ });
4890
+ \`\`\`
4891
+
4892
+ ## E-Commerce Schema
4893
+
4894
+ \`\`\`sql
4895
+ -- Products
4896
+ CREATE TABLE products (
4897
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4898
+ name TEXT NOT NULL,
4899
+ slug TEXT UNIQUE NOT NULL,
4900
+ description TEXT,
4901
+ price NUMERIC(10,2) NOT NULL,
4902
+ compare_at_price NUMERIC(10,2),
4903
+ currency TEXT DEFAULT 'USD',
4904
+ sku TEXT UNIQUE,
4905
+ inventory_count INTEGER DEFAULT 0,
4906
+ category_id UUID REFERENCES categories(id),
4907
+ images JSONB DEFAULT '[]', -- [{url, alt, position}]
4908
+ metadata JSONB DEFAULT '{}',
4909
+ status TEXT DEFAULT 'draft', -- draft, active, archived
4910
+ created_at TIMESTAMPTZ DEFAULT now(),
4911
+ updated_at TIMESTAMPTZ DEFAULT now()
4912
+ );
4913
+ CREATE INDEX idx_products_category ON products(category_id);
4914
+ CREATE INDEX idx_products_status ON products(status);
4915
+
4916
+ -- Categories
4917
+ CREATE TABLE categories (
4918
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4919
+ name TEXT NOT NULL,
4920
+ slug TEXT UNIQUE NOT NULL,
4921
+ parent_id UUID REFERENCES categories(id),
4922
+ sort_order INTEGER DEFAULT 0
4923
+ );
4924
+
4925
+ -- Orders
4926
+ CREATE TABLE orders (
4927
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4928
+ user_id UUID NOT NULL,
4929
+ status TEXT DEFAULT 'pending', -- pending, confirmed, shipped, delivered, cancelled
4930
+ subtotal NUMERIC(10,2) NOT NULL,
4931
+ tax NUMERIC(10,2) DEFAULT 0,
4932
+ shipping NUMERIC(10,2) DEFAULT 0,
4933
+ total NUMERIC(10,2) NOT NULL,
4934
+ currency TEXT DEFAULT 'USD',
4935
+ shipping_address JSONB,
4936
+ billing_address JSONB,
4937
+ stripe_payment_intent_id TEXT,
4938
+ notes TEXT,
4939
+ created_at TIMESTAMPTZ DEFAULT now(),
4940
+ updated_at TIMESTAMPTZ DEFAULT now()
4941
+ );
4942
+ CREATE INDEX idx_orders_user ON orders(user_id);
4943
+ CREATE INDEX idx_orders_status ON orders(status);
4944
+
4945
+ -- Order items
4946
+ CREATE TABLE order_items (
4947
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4948
+ order_id UUID REFERENCES orders(id) ON DELETE CASCADE,
4949
+ product_id UUID REFERENCES products(id),
4950
+ quantity INTEGER NOT NULL,
4951
+ unit_price NUMERIC(10,2) NOT NULL,
4952
+ total NUMERIC(10,2) NOT NULL
4953
+ );
4954
+ CREATE INDEX idx_order_items_order ON order_items(order_id);
4955
+
4956
+ -- Cart (session-based)
4957
+ CREATE TABLE carts (
4958
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4959
+ user_id UUID,
4960
+ session_id TEXT,
4961
+ items JSONB DEFAULT '[]', -- [{productId, quantity, price}]
4962
+ created_at TIMESTAMPTZ DEFAULT now(),
4963
+ updated_at TIMESTAMPTZ DEFAULT now()
4964
+ );
4965
+
4966
+ -- Reviews
4967
+ CREATE TABLE reviews (
4968
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4969
+ product_id UUID REFERENCES products(id) ON DELETE CASCADE,
4970
+ user_id UUID NOT NULL,
4971
+ rating INTEGER CHECK (rating >= 1 AND rating <= 5),
4972
+ title TEXT,
4973
+ body TEXT,
4974
+ created_at TIMESTAMPTZ DEFAULT now()
4975
+ );
4976
+ CREATE INDEX idx_reviews_product ON reviews(product_id);
4977
+ \`\`\`
4978
+
4979
+ ## Common Patterns
4980
+
4981
+ ### Product Listing with Filters
4982
+
4983
+ \`\`\`typescript
4984
+ // Browse products with category filter and pagination
4985
+ const { data: products } = await vaif
4986
+ .from("products")
4987
+ .select("id, name, slug, price, compare_at_price, images, category:categories(name)")
4988
+ .eq("status", "active")
4989
+ .eq("category_id", categoryId)
4990
+ .order("created_at", { ascending: false })
4991
+ .limit(20)
4992
+ .offset(page * 20);
4993
+ \`\`\`
4994
+
4995
+ ### Cart Management
4996
+
4997
+ \`\`\`typescript
4998
+ // Add to cart
4999
+ const cart = await vaif.from("carts").select().eq("user_id", userId).single();
5000
+ const items = [...(cart.data?.items || [])];
5001
+ const existing = items.findIndex(i => i.productId === productId);
5002
+ if (existing >= 0) {
5003
+ items[existing].quantity += quantity;
5004
+ } else {
5005
+ items.push({ productId, quantity, price });
5006
+ }
5007
+ await vaif.from("carts").update({ items, updated_at: new Date().toISOString() }).eq("id", cart.data.id);
5008
+ \`\`\`
5009
+
5010
+ ### Order Creation
5011
+
5012
+ \`\`\`typescript
5013
+ // Create order from cart
5014
+ const { data: order } = await vaif.from("orders").insert({
5015
+ user_id: userId,
5016
+ subtotal, tax, shipping,
5017
+ total: subtotal + tax + shipping,
5018
+ shipping_address: addressData,
5019
+ });
5020
+
5021
+ // Create order items
5022
+ const orderItems = cartItems.map(item => ({
5023
+ order_id: order.id,
5024
+ product_id: item.productId,
5025
+ quantity: item.quantity,
5026
+ unit_price: item.price,
5027
+ total: item.price * item.quantity,
5028
+ }));
5029
+ await vaif.from("order_items").insert(orderItems);
5030
+ \`\`\`
5031
+
5032
+ ### Inventory Management
5033
+
5034
+ \`\`\`typescript
5035
+ // Decrement inventory on order (VAIF Function)
5036
+ export default async function handler(req, ctx) {
5037
+ const { items } = req.body; // [{productId, quantity}]
5038
+ for (const item of items) {
5039
+ const { data: product } = await vaif.from("products")
5040
+ .select("inventory_count")
5041
+ .eq("id", item.productId)
5042
+ .single();
5043
+
5044
+ if (product.inventory_count < item.quantity) {
5045
+ return { error: \\\`Insufficient stock for \${item.productId}\\\` };
5046
+ }
5047
+
5048
+ await vaif.from("products")
5049
+ .update({ inventory_count: product.inventory_count - item.quantity })
5050
+ .eq("id", item.productId);
5051
+ }
5052
+ return { success: true };
5053
+ }
5054
+ \`\`\`
5055
+
5056
+ ### Payment Webhooks
5057
+
5058
+ \`\`\`typescript
5059
+ // Stripe webhook handler (VAIF Function)
5060
+ export default async function handler(req, ctx) {
5061
+ const sig = req.headers["stripe-signature"];
5062
+ const event = stripe.webhooks.constructEvent(req.rawBody, sig, ctx.secrets.STRIPE_WEBHOOK_SECRET);
5063
+
5064
+ switch (event.type) {
5065
+ case "payment_intent.succeeded":
5066
+ await vaif.from("orders")
5067
+ .update({ status: "confirmed" })
5068
+ .eq("stripe_payment_intent_id", event.data.object.id);
5069
+ break;
5070
+ case "payment_intent.payment_failed":
5071
+ await vaif.from("orders")
5072
+ .update({ status: "cancelled" })
5073
+ .eq("stripe_payment_intent_id", event.data.object.id);
5074
+ break;
5075
+ }
5076
+ return { received: true };
5077
+ }
5078
+ \`\`\`
5079
+
5080
+ ### Product Image Upload
5081
+
5082
+ \`\`\`typescript
5083
+ // Upload product images to storage
5084
+ const { url } = await vaif.storage
5085
+ .from("product-images")
5086
+ .upload(\\\`\${productId}/\${fileName}\\\`, file, { contentType: "image/webp" });
5087
+
5088
+ // Update product images array
5089
+ const { data: product } = await vaif.from("products").select("images").eq("id", productId).single();
5090
+ const images = [...(product.images || []), { url, alt: altText, position: product.images.length }];
5091
+ await vaif.from("products").update({ images }).eq("id", productId);
5092
+ \`\`\`
5093
+
5094
+ ## Environment Variables
5095
+
5096
+ \`\`\`bash
5097
+ VAIF_API_URL=https://api.vaif.studio
5098
+ VAIF_PROJECT_ID=proj_xxx
5099
+ VAIF_API_KEY=vk_xxx
5100
+ STRIPE_SECRET_KEY=sk_live_xxx
5101
+ STRIPE_PUBLISHABLE_KEY=pk_live_xxx
5102
+ STRIPE_WEBHOOK_SECRET=whsec_xxx
5103
+ \`\`\`
5104
+
5105
+ > **Note**: \`numeric\` columns (like \`price\`) serialize to JSON strings to preserve precision. Parse with \`parseFloat()\` before arithmetic.
5106
+ `};var Ve={$schema:"https://vaif.studio/schemas/config.json",projectId:"",database:{url:"${DATABASE_URL}",schema:"public"},types:{output:"./src/types/database.ts"},api:{baseUrl:"https://api.vaif.studio"}};async function Ot(n){if(n.claude!==void 0){let t=typeof n.claude=="string"?n.claude:null;if(t&&t in k){let i=j("Generating CLAUDE.md from template...").start(),s=k[t],l=Oe();l&&(s+=`
5107
+ ## Team Conventions
5108
+
5109
+ ${l}
5110
+ `);let r=I.resolve("CLAUDE.md");v.existsSync(r)&&!n.force&&(i.fail("CLAUDE.md already exists"),console.log(u.yellow(`
5111
+ Use --force to overwrite.`)),process.exit(1)),v.writeFileSync(r,s,"utf-8"),i.succeed(`Created CLAUDE.md (${t} template)`),l&&console.log(u.gray(" Imported team conventions from existing config files.")),console.log(u.gray(`
5112
+ Customize the generated CLAUDE.md with your project details.`)),console.log(u.gray(` For a personalized version from live data, run: vaif claude-setup
5113
+ `));return}t&&!(t in k)&&(console.log(u.red(`
5114
+ Unknown template type: "${t}"`)),console.log(u.gray(" Available types: base, saas, mobile, ecommerce")),console.log(u.gray(` Or omit the type to auto-generate from your live project: vaif init --claude
5115
+ `)),process.exit(1));let o=je();o?(console.log(u.gray(`
5116
+ Detected project type: ${u.cyan(o)}`)),console.log(u.gray(` Generating from live project data with template context...
5117
+ `))):console.log(u.gray(`
5118
+ Generating from live project data...
5119
+ `)),await ne({skipMcp:false});return}if(n.addFeatures){n.template||(console.log(u.red(`
5120
+ --add-features requires --template to know which template to use.`)),console.log(u.gray("Example: vaif init --template react-spa --add-features functions,storage")),process.exit(1));let t=n.addFeatures.split(",").map(o=>o.trim());await q(n.template,{force:n.force,features:t,addOnly:true});return}let e=j("Initializing VAIF configuration...").start(),a=I.resolve("vaif.config.json");v.existsSync(a)&&!n.force&&(e.fail("vaif.config.json already exists"),console.log(u.yellow(`
5121
+ Use --force to overwrite existing configuration.`)),process.exit(1));try{if(v.writeFileSync(a,JSON.stringify(Ve,null,2),"utf-8"),e.succeed("Created vaif.config.json"),n.template){let t=n.features?n.features.split(",").map(o=>o.trim()):void 0;await q(n.template,{force:n.force,features:t});}else {let t=I.resolve(".env.example");if(v.existsSync(t)||(v.writeFileSync(t,`# VAIF Configuration
3786
5122
  DATABASE_URL=postgresql://user:password@localhost:5432/database
3787
5123
  VAIF_API_KEY=your-api-key
3788
- `,"utf-8"),console.log(l.gray("Created .env.example"))),a.typescript){let r=k.resolve("src/types");b.existsSync(r)||(b.mkdirSync(r,{recursive:!0}),console.log(l.gray("Created src/types directory")));}console.log(""),console.log(l.green("VAIF initialized successfully!")),console.log(""),console.log(l.gray("Next steps:")),console.log(l.gray(" 1. Update vaif.config.json with your project ID")),console.log(l.gray(" 2. Set DATABASE_URL in your environment")),console.log(l.gray(" 3. Run: npx vaif generate")),console.log("");}}catch(e){t.fail("Failed to initialize"),e instanceof Error&&console.error(l.red(`
3789
- Error: ${e.message}`)),process.exit(1);}}export{q as a,V as b,Ue as c,De as d,ke as e,He as f,tt as g,ct as h};
5124
+ `,"utf-8"),console.log(u.gray("Created .env.example"))),n.typescript){let o=I.resolve("src/types");v.existsSync(o)||(v.mkdirSync(o,{recursive:!0}),console.log(u.gray("Created src/types directory")));}console.log(""),console.log(u.green("VAIF initialized successfully!")),console.log(""),console.log(u.gray("Next steps:")),console.log(u.gray(" 1. Update vaif.config.json with your project ID")),console.log(u.gray(" 2. Set DATABASE_URL in your environment")),console.log(u.gray(" 3. Run: npx vaif generate")),console.log("");}}catch(t){e.fail("Failed to initialize"),t instanceof Error&&console.error(u.red(`
5125
+ Error: ${t.message}`)),process.exit(1);}}function je(){try{let n=I.resolve("package.json");if(!v.existsSync(n))return null;let e=JSON.parse(v.readFileSync(n,"utf-8")),a={...e.dependencies,...e.devDependencies};return a.expo||a["react-native"]||a["@vaiftech/sdk-expo"]||v.existsSync(I.resolve("pubspec.yaml"))||v.existsSync(I.resolve("Package.swift"))||v.existsSync(I.resolve("*.xcodeproj"))?"mobile":(a.stripe||a["@stripe/stripe-js"]||a["shopify-api-node"]||a["@shopify/shopify-api"])&&(v.existsSync(I.resolve("src/models/product.ts"))||v.existsSync(I.resolve("src/models/order.ts"))||v.existsSync(I.resolve("src/pages/products"))||v.existsSync(I.resolve("src/pages/cart")))?"ecommerce":a.stripe||a["@stripe/stripe-js"]||e.name?.includes("saas")||e.description?.toLowerCase().includes("saas")?"saas":"base"}catch{return null}}function Oe(){let n=[{path:".cursorrules",label:"Cursor Rules"},{path:".github/copilot-instructions.md",label:"GitHub Copilot Instructions"},{path:".windsurfrules",label:"Windsurf Rules"},{path:".clinerules",label:"Cline Rules"}],e=[];for(let a of n){let t=I.resolve(a.path);if(v.existsSync(t))try{let o=v.readFileSync(t,"utf-8").trim();o.length>0&&o.length<1e4&&e.push(`### From ${a.label} (\`${a.path}\`)
5126
+
5127
+ ${o}`);}catch{}}return e.length>0?e.join(`
5128
+
5129
+ `):null}export{N as a,R as b,Qe as c,Ze as d,et as e,ut as f,vt as g,ne as h,Ot as i};