create-instant-app 1.0.39 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,34 +1,34 @@
1
1
  #!/usr/bin/env node
2
- import{Command as Me,Option as l}from"commander";import*as B from"@clack/prompts";import{spawn as je}from"child_process";import{execa as Fe}from"execa";async function U(){return new Promise(e=>{let t=process.env.SHELL||"/bin/bash",n=je(t,["-i","-c","which claude"],{stdio:["ignore","pipe","ignore"]}),r="";n.stdout.on("data",i=>{r+=i.toString()}),n.on("close",i=>{if(i===0){let o=r.trim();o.includes("aliased to ")?e(o.split("aliased to ")[1]||null):e(o)}else e(null)})})}var H=async(e,t)=>{let n=await U();if(!n)throw new Error("Claude not found in path");await Fe(n,[e],{stdio:"inherit",cwd:t})};import{version as $e}from"@instantdb/version";import Y from"path";var q=e=>(e.length>1&&e.endsWith("/")&&(e=e.slice(0,-1)),e),Re=/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,X=e=>{let n=q(e).split("/"),r=n[n.length-1];if(r==="."){let s=Y.resolve(process.cwd());r=Y.basename(s)}let i=n.findIndex(s=>s.startsWith("@"));n.findIndex(s=>s.startsWith("@"))!==-1&&(r=n.slice(i).join("/"));let o=n.filter(s=>!s.startsWith("@")).join("/");return[r,o]},j=e=>e.trim(),V=e=>{let t=q(e),n=t.split("/"),r=n.findIndex(o=>o.startsWith("@")),i=n[n.length-1];if(n.findIndex(o=>o.startsWith("@"))!==-1&&(i=n.slice(r).join("/")),!(t==="."||Re.test(i??"")))return"App name must consist of only lowercase alphanumeric characters, '-', and '_'"};import{renderUnwrap as N,UI as f}from"instant-cli/ui";var F={base:"next-js-app-dir",appName:"Awesome Todos",ruleFiles:null,createRepo:!0,prompt:null,app:null,token:null,yes:!1,temporary:!1},Z=e=>e.base||e.viteReact&&"vite-react"||e.vanilla&&"vite-vanilla"||e.next&&"next-js-app-dir"||e.expo&&"expo"||e.sv&&"sveltekit"||e.vue&&"vue-vite"||null,Q=e=>e.cursor&&"cursor"||e.claude&&"claude"||e.codex&&"codex"||e.gemini&&"gemini"||e.rules&&"codex"||null,ee=async()=>{let e=F,t=new Me().name("Create Instant App").description("A CLI for creating web/mobile applications with InstantDB").argument("[dir]","The name of the application, as well as the name of the directory to create").addOption(new l("-b --base <template>","The base template to scaffold from").choices(["next-js-app-dir","vite-react","vite-vanilla","expo","bun-react","tanstack-start","tanstack-start-with-tanstack-query","solidjs-vite","sveltekit","vue-vite","vercel-ai-sdk","ai-chat"])).addOption(new l("-g --git","Create a git repo in the new project").default(!0)).addOption(new l("--expo","Use the Expo starter template").default(!1)).addOption(new l("--next","Use the NextJS starter template").default(!1)).addOption(new l("--vanilla","Use the vanilla JS starter template").default(!1)).addOption(new l("--vite-react","Use the Vite + React starter template").default(!1)).addOption(new l("--sv","Use the SvelteKit starter template").default(!1)).addOption(new l("--vue","Use the Vue + Vite starter template").default(!1)).addOption(new l("--no-git","Don't create a git repo in the new project")).addOption(new l("--cursor","Include a Cursor rules file in the scaffold")).addOption(new l("--claude","Include a CLAUDE.md file in the scaffold")).addOption(new l("--codex","Include an AGENTS.md file in the scaffold")).addOption(new l("--gemini","Include a GEMINI.md file in the scaffold")).addOption(new l("--rules","Include an AGENTS.md file in the scaffold")).addOption(new l("--ai","Create a new InstantDB app based off of a prompt. (requires Claude Code)")).addOption(new l("-a --app <app-id>","Link to an existing InstantDB app by ID (requires login or --token)")).addOption(new l("-t --token <token>","Auth token override (use with --app when not logged in)")).addOption(new l("-y --yes","Use all defaults (requires project name as first argument)").default(!1)).addOption(new l("--temp","Create a temporary app by default").default(!1)).version($e).parse(process.argv),n=t.args[0]&&j(t.args[0]);if(n){let o=V(n);if(o)throw new Error("Invalid app name: "+o);e.appName=n}let r=t.opts();if(r.app&&r.temp)throw new Error("--app and --temp cannot be used together");if(r.yes){if(r.ai)throw new Error("--yes is not supported with --ai");if(!n)throw new Error(`When using --yes, you must specify a project name as the first argument.
3
- Usage: npx create-instant-app my-app --yes`);return{...F,appName:n,base:Z(r)??F.base,ruleFiles:Q(r)??"claude",createRepo:r.git??F.createRepo,app:r.app??null,token:r.token??null,yes:!0,temporary:r.temp??!1}}if(r.ai&&!await U())throw new Error("--ai only works with Claude Code, but we couldn't find it in your machine. Install it first, and run it again : ). Alternatively you can scaffold out a project without --ai");return{...await B.group({appName:async()=>{if(n)return n.trim();let o=await N(new f.TextInput({prompt:"What will your project/folder be called?",placeholder:"awesome-todos",defaultValue:"awesome-todos",validate:a=>V(j(a)),modifyOutput:f.ciaModifier()}));return j(o)},prompt:async()=>r.ai?await N(new f.TextInput({prompt:"What would you like to create?",placeholder:"Create an app that...",modifyOutput:f.modifiers.piped([f.ciaModifier()])})):null,base:async({results:o})=>{let s=Z(r);return s||(o.prompt?N(new f.Select({promptText:"What framework would you like to use?",options:[{value:"next-js-app-dir",label:"Web: Next.js"},{value:"vite-react",label:"Web: Vite React"},{value:"expo",label:"Mobile: Expo"}],defaultValue:"next-js-app-dir",modifyOutput:f.modifiers.piped([f.ciaModifier()])})):N(new f.Select({promptText:"What framework would you like to use?",options:[{value:"next-js-app-dir",label:"Web: Next.js"},{value:"expo",label:"Mobile: Expo"},{value:"vite-react",label:"Vite: React",secondary:!0},{value:"vite-vanilla",label:"Vite: Vanilla TS",secondary:!0},{value:"tanstack-start",label:"Tanstack Start",secondary:!0},{value:"bun-react",label:"Bun + React",secondary:!0},{value:"solidjs-vite",label:"Vite: SolidJS",secondary:!0},{value:"sveltekit",label:"SvelteKit",secondary:!0},{value:"vue-vite",label:"Vite: Vue",secondary:!0},{value:"vercel-ai-sdk",label:"Vercel AI SDK App Builder + SSR",secondary:!0},{value:"ai-chat",label:"Vercel AI SDK Chat App",secondary:!0}],defaultValue:"next-js-app-dir",modifyOutput:f.modifiers.piped([f.ciaModifier()])})))},ruleFiles:async({results:o})=>{if(o.prompt)return"claude";let s=Q(r);return s||N(new f.Select({promptText:"Which AI tool would you like to add rule files for?",options:[{value:"claude",label:"Claude"},{value:"cursor",label:"Cursor"},{value:"codex",label:"Codex"},{value:"gemini",label:"Gemini"},{value:"zed",label:"Zed"},{value:"windsurf",label:"Windsurf"},{value:null,label:"None"}],defaultValue:"claude",modifyOutput:f.ciaModifier()}))},createRepo:async()=>r.git!==void 0?r.git:!0,temporary:async()=>r.temp??!1},{onCancel(){process.exit(1)}}),app:r.app??null,token:r.token??null,yes:!1}};import Ne from"path";import Ee from"fs-extra";import{log as Ce,outro as vt}from"@clack/prompts";import{intro as De}from"@clack/prompts";import te from"chalk";var Ue=` _ _ _
2
+ import{Command as Fe,Option as l}from"commander";import*as U from"@clack/prompts";import{spawn as Oe}from"child_process";import{execa as je}from"execa";async function $(){return new Promise(e=>{let t=process.env.SHELL||"/bin/bash",n=Oe(t,["-i","-c","which claude"],{stdio:["ignore","pipe","ignore"]}),r="";n.stdout.on("data",i=>{r+=i.toString()}),n.on("close",i=>{if(i===0){let o=r.trim();o.includes("aliased to ")?e(o.split("aliased to ")[1]||null):e(o)}else e(null)})})}var J=async(e,t)=>{let n=await $();if(!n)throw new Error("Claude not found in path");await je(n,[e],{stdio:"inherit",cwd:t})};import{version as Me}from"@instantdb/version";import K from"path";var H=e=>(e.length>1&&e.endsWith("/")&&(e=e.slice(0,-1)),e),Re=/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,Y=e=>{let n=H(e).split("/"),r=n[n.length-1];if(r==="."){let s=K.resolve(process.cwd());r=K.basename(s)}let i=n.findIndex(s=>s.startsWith("@"));n.findIndex(s=>s.startsWith("@"))!==-1&&(r=n.slice(i).join("/"));let o=n.filter(s=>!s.startsWith("@")).join("/");return[r,o]},E=e=>e.trim(),D=e=>{let t=H(e),n=t.split("/"),r=n.findIndex(o=>o.startsWith("@")),i=n[n.length-1];if(n.findIndex(o=>o.startsWith("@"))!==-1&&(i=n.slice(r).join("/")),!(t==="."||Re.test(i??"")))return"App name must consist of only lowercase alphanumeric characters, '-', and '_'"};import{renderUnwrap as P,UI as m}from"instant-cli/ui";var C={base:"next-js-app-dir",appName:"Awesome Todos",ruleFiles:null,createRepo:!0,prompt:null,app:null,token:null,yes:!1,temporary:!1},q=e=>e.base||e.viteReact&&"vite-react"||e.vanilla&&"vite-vanilla"||e.next&&"next-js-app-dir"||e.expo&&"expo"||e.sv&&"sveltekit"||e.vue&&"vue-vite"||null,X=e=>e.cursor&&"cursor"||e.claude&&"claude"||e.codex&&"codex"||e.gemini&&"gemini"||e.rules&&"codex"||null,Z=async()=>{let e=C,t=new Fe().name("Create Instant App").description("A CLI for creating web/mobile applications with InstantDB").argument("[dir]","The name of the application, as well as the name of the directory to create").addOption(new l("-b --base <template>","The base template to scaffold from").choices(["next-js-app-dir","vite-react","vite-vanilla","expo","bun-react","tanstack-start","tanstack-start-with-tanstack-query","solidjs-vite","sveltekit","vue-vite","vercel-ai-sdk","ai-chat"])).addOption(new l("-g --git","Create a git repo in the new project").default(!0)).addOption(new l("--expo","Use the Expo starter template").default(!1)).addOption(new l("--next","Use the NextJS starter template").default(!1)).addOption(new l("--vanilla","Use the vanilla JS starter template").default(!1)).addOption(new l("--vite-react","Use the Vite + React starter template").default(!1)).addOption(new l("--sv","Use the SvelteKit starter template").default(!1)).addOption(new l("--vue","Use the Vue + Vite starter template").default(!1)).addOption(new l("--no-git","Don't create a git repo in the new project")).addOption(new l("--cursor","Include a Cursor rules file in the scaffold")).addOption(new l("--claude","Include a CLAUDE.md file in the scaffold")).addOption(new l("--codex","Include an AGENTS.md file in the scaffold")).addOption(new l("--gemini","Include a GEMINI.md file in the scaffold")).addOption(new l("--rules","Include an AGENTS.md file in the scaffold")).addOption(new l("--ai","Create a new InstantDB app based off of a prompt. (requires Claude Code)")).addOption(new l("-a --app <app-id>","Link to an existing InstantDB app by ID (requires login or --token)")).addOption(new l("-t --token <token>","Auth token override (use with --app when not logged in)")).addOption(new l("-y --yes","Use all defaults (requires project name as first argument)").default(!1)).addOption(new l("--temp","Create a temporary app by default").default(!1)).version(Me).parse(process.argv),n=t.args[0]&&E(t.args[0]);if(n){let o=D(n);if(o)throw new Error("Invalid app name: "+o);e.appName=n}let r=t.opts();if(r.app&&r.temp)throw new Error("--app and --temp cannot be used together");if(r.yes){if(r.ai)throw new Error("--yes is not supported with --ai");if(!n)throw new Error(`When using --yes, you must specify a project name as the first argument.
3
+ Usage: npx create-instant-app my-app --yes`);return{...C,appName:n,base:q(r)??C.base,ruleFiles:X(r)??"claude",createRepo:r.git??C.createRepo,app:r.app??null,token:r.token??null,yes:!0,temporary:r.temp??!1}}if(r.ai&&!await $())throw new Error("--ai only works with Claude Code, but we couldn't find it in your machine. Install it first, and run it again : ). Alternatively you can scaffold out a project without --ai");return{...await U.group({appName:async()=>{if(n)return n.trim();let o=await P(new m.TextInput({prompt:"What will your project/folder be called?",placeholder:"awesome-todos",defaultValue:"awesome-todos",validate:a=>D(E(a)),modifyOutput:m.ciaModifier()}));return E(o)},prompt:async()=>r.ai?await P(new m.TextInput({prompt:"What would you like to create?",placeholder:"Create an app that...",modifyOutput:m.modifiers.piped([m.ciaModifier()])})):null,base:async({results:o})=>{let s=q(r);return s||(o.prompt?P(new m.Select({promptText:"What framework would you like to use?",options:[{value:"next-js-app-dir",label:"Web: Next.js"},{value:"vite-react",label:"Web: Vite React"},{value:"expo",label:"Mobile: Expo"}],defaultValue:"next-js-app-dir",modifyOutput:m.modifiers.piped([m.ciaModifier()])})):P(new m.Select({promptText:"What framework would you like to use?",options:[{value:"next-js-app-dir",label:"Web: Next.js"},{value:"expo",label:"Mobile: Expo"},{value:"vite-react",label:"Vite: React",secondary:!0},{value:"vite-vanilla",label:"Vite: Vanilla TS",secondary:!0},{value:"tanstack-start",label:"Tanstack Start",secondary:!0},{value:"bun-react",label:"Bun + React",secondary:!0},{value:"solidjs-vite",label:"Vite: SolidJS",secondary:!0},{value:"sveltekit",label:"SvelteKit",secondary:!0},{value:"vue-vite",label:"Vite: Vue",secondary:!0},{value:"vercel-ai-sdk",label:"Vercel AI SDK App Builder + SSR",secondary:!0},{value:"ai-chat",label:"Vercel AI SDK Chat App",secondary:!0}],defaultValue:"next-js-app-dir",modifyOutput:m.modifiers.piped([m.ciaModifier()])})))},ruleFiles:async({results:o})=>{if(o.prompt)return"claude";let s=X(r);return s||P(new m.Select({promptText:"Which AI tool would you like to add rule files for?",options:[{value:"claude",label:"Claude"},{value:"cursor",label:"Cursor"},{value:"codex",label:"Codex"},{value:"gemini",label:"Gemini"},{value:"zed",label:"Zed"},{value:"windsurf",label:"Windsurf"},{value:null,label:"None"}],defaultValue:"claude",modifyOutput:m.ciaModifier()}))},createRepo:async()=>r.git!==void 0?r.git:!0,temporary:async()=>r.temp??!1},{onCancel(){process.exit(1)}}),app:r.app??null,token:r.token??null,yes:!1}};import Se from"path";import Ne from"fs-extra";import{log as Ee,outro as Pt}from"@clack/prompts";import{intro as $e}from"@clack/prompts";import Q from"chalk";var De=` _ _ _
4
4
  \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 (_) | | | |
5
5
  \u2588 \u2588\u2588\u2588\u2588 _ _ __ ___| |_ __ _ _ _ _| |_
6
6
  \u2588 \u2588\u2588\u2588\u2588 | | '_ \\/ __| __/ _\\\`| '_ \\| __|
7
7
  \u2588 \u2588\u2588\u2588\u2588 | | | | \\__ \\ || (_| | | | | |_
8
- \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 |_|_| |_|___/\\__\\__,_|_| |_|\\__|`,ne=()=>{De(`
9
- `+Ue.split(`
10
- `).map(e=>`${te.gray("\u2502")}${te.hex("#EA580D").bold(" "+e)}`).join(`
11
- `))};import u from"path";import Le from"tiged";import c from"fs-extra";import re from"path";import{fileURLToPath as Ve}from"url";var Be=Ve(import.meta.url),Ge=re.dirname(Be),w=re.join(Ge,"../");import x from"chalk";var I=e=>{if(e==="bun-react")return"bun";let t=process.env.npm_config_user_agent;return t?t.startsWith("yarn")?"yarn":t.startsWith("pnpm")?"pnpm":t.startsWith("bun")?"bun":"npm":"npm"};import{renderUnwrap as oe,UI as y}from"instant-cli/ui";import We from"slugify";import ze from"ignore";var ae=async(e,t)=>{let n=u.resolve(process.cwd(),t);if(c.existsSync(n))if(c.readdirSync(n).length===0)y.log(`${x.cyan.bold(e.appName)} exists but is empty, continuing...`,y.ciaModifier(null));else{let o=await oe(new y.Select({promptText:x.redBright(`${x.bold("Warning:")} ${x.bold(e.appName)} already exists and isn't empty. How would you like to proceed?`),options:[{label:"Abort installation",value:"abort"},{label:"Clear the directory and continue installation",value:"clear"}],defaultValue:"abort",modifyOutput:y.ciaModifier()}));o==="abort"&&(y.log("Aborting installation..."),process.exit(1)),o==="clear"&&c.emptyDirSync(n)}let r=He({projectDir:n,baseTemplateName:e.base}),i=e.appName==="."?"App":x.hex("#EA570B").bold(e.appName);if(await oe(new y.Spinner({promise:r,workingText:"Scaffolding project files...",doneText:`Successfully scaffolded ${i}!`,errorText:"Error scaffolding project files",modifyOutput:y.ciaModifier(null)})),c.pathExistsSync(u.join(n,"pnpm-lock.yaml"))&&c.removeSync(u.join(n,"pnpm-lock.yaml")),c.pathExistsSync(u.join(n,"bun.lock"))&&c.removeSync(u.join(n,"bun.lock")),I(e.base)==="pnpm"&&e.base==="expo"&&await c.appendFile(u.join(n,".npmrc"),`node-linker=hoisted
12
- enable-pre-post-scripts=true`),e.base==="expo"){let o=We.default(t);G(u.join(n,"app.json"),'"name": "expo-template"',`"name": "${t}"`),G(u.join(n,"app.json"),'"slug": "expo-template"',`"slug": "${o}"`),G(u.join(n,"app/_layout.tsx"),'"My Instant App"',`"${t}"`)}return n},G=(e,t,n)=>{let i=c.readFileSync(e,"utf8").replaceAll(t,n);c.writeFileSync(e,i)},Je=async({projectDir:e,baseTemplateName:t})=>{let n=`instantdb/instant/examples/${t}`;await Le(n,{mode:"tar",disableCache:!0}).clone(e)};async function Ke(e,t){let n=ze();n.add(".git");try{let r=u.join(e,".gitignore"),i=await c.readFile(r,"utf8");n.add(i)}catch{}await c.copy(e,t,{filter:r=>{let i=u.relative(e,r);return i===""?!0:!n.ignores(i)}})}var He=async({projectDir:e,baseTemplateName:t})=>{let n=u.join(w,`template/base/${t}`),r=!!process.env.INSTANT_CLI_DEV&&!!process.env.INSTANT_REPO_FOLDER,i=c.pathExistsSync(n),o=r?"dev":i?"bundled-template":"tiged";if(o==="bundled-template"){c.copySync(n,e);let s=u.join(e,"_gitignore"),a=u.join(e,".gitignore");c.pathExistsSync(s)&&c.renameSync(s,a);return}if(o==="dev"){let s=process.env.INSTANT_REPO_FOLDER;if(!s)throw new Error("INSTANT_REPO_FOLDER is required when using repo-examples scaffolding.");let a=u.join(s,"examples",t);await Ke(a,e);return}process.env.INSTANT_CLI_DEV&&!process.env.INSTANT_REPO_FOLDER&&y.log(x.bold.yellowBright(`WARNING: INSTANT_CLI_DEV is TRUE but no INSTANT_REPO_FOLDER is set.
13
- Using git to clone from main...`),y.ciaModifier(null)),await Je({projectDir:e,baseTemplateName:t})};import{execa as Ye}from"execa";import{renderUnwrap as qe,UI as R}from"instant-cli/ui";var ie=async(e,t)=>{let n=Ye(e,["install"],{cwd:t}),r=await qe(new R.Spinner({promise:n,workingText:`Installing dependencies with ${e}...`,doneText:"Successfully installed dependencies!",modifyOutput:R.ciaModifier(null)}));r.exitCode!==0&&(R.log(r.stderr,R.ciaModifier(null)),process.exit(1))};import St from"chalk";import T from"fs-extra";import m from"path";var se=({projectDir:e,ruleFilesToAdd:t})=>{if(t!==null)switch(t){case"cursor":T.ensureDirSync(m.join(e,".cursor/rules")),T.copyFileSync(m.join(w,"template/rules/cursor-rules.md"),m.join(e,".cursor/rules/instant.mdc"));break;case"claude":T.copyFileSync(m.join(w,"template/rules/AGENTS.md"),m.join(e,"CLAUDE.md"));break;case"codex":T.copyFileSync(m.join(w,"template/rules/AGENTS.md"),m.join(e,"AGENTS.md"));break;case"gemini":T.copyFileSync(m.join(w,"template/rules/AGENTS.md"),m.join(e,"GEMINI.md"));break;case"zed":T.copyFileSync(m.join(w,"template/rules/AGENTS.md"),m.join(e,"AGENTS.md"));break;case"windsurf":T.ensureDirSync(m.join(e,".windsurf/rules")),T.copyFileSync(m.join(w,"template/rules/windsurf-rules.md"),m.join(e,".windsurf/rules/instant.md"));break}};import{execSync as W}from"child_process";import L from"path";import*as le from"@clack/prompts";import M from"chalk";import{execa as S}from"execa";import ce from"fs-extra";import{renderUnwrap as pe,UI as v}from"instant-cli/ui";var Xe=e=>{try{return W("git --version",{cwd:e}),!0}catch{return!1}},Ze=e=>ce.existsSync(L.join(e,".git")),Qe=async e=>{try{return await S("git",["rev-parse","--is-inside-work-tree"],{cwd:e,stdout:"ignore"}),!0}catch{return!1}},et=()=>{let t=W("git --version").toString().trim().split(" ")[2],n=t?.split(".")[0],r=t?.split(".")[1];return{major:Number(n),minor:Number(r)}},tt=()=>W("git config --global init.defaultBranch || echo main").toString().trim(),de=async e=>{if(!Xe(e)){le.log.warn("Git is not installed. Skipping Git initialization.");return}let t=Ze(e),n=await Qe(e),r=L.parse(e).name;if(n&&t){if(!await pe(new v.Confirmation({promptText:`${M.redBright.bold("Warning:")} Git is already initialized in "${r}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`,defaultValue:!1,modifyOutput:v.ciaModifier()})))return;ce.removeSync(L.join(e,".git"))}else if(n&&!t&&!await pe(new v.Confirmation({promptText:`${M.redBright.bold("Warning:")} "${r}" is already in a git worktree.
8
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 |_|_| |_|___/\\__\\__,_|_| |_|\\__|`,ee=()=>{$e(`
9
+ `+De.split(`
10
+ `).map(e=>`${Q.gray("\u2502")}${Q.hex("#EA580D").bold(" "+e)}`).join(`
11
+ `))};import u from"path";import Ge from"tiged";import c from"fs-extra";import te from"path";import{fileURLToPath as Ue}from"url";var Ve=Ue(import.meta.url),Be=te.dirname(Ve),O=te.join(Be,"../");import I from"chalk";var T=e=>{if(e==="bun-react")return"bun";let t=process.env.npm_config_user_agent;return t?t.startsWith("yarn")?"yarn":t.startsWith("pnpm")?"pnpm":t.startsWith("bun")?"bun":"npm":"npm"};import{renderUnwrap as ne,UI as h}from"instant-cli/ui";import Le from"slugify";import We from"ignore";var re=async(e,t)=>{let n=u.resolve(process.cwd(),t);if(c.existsSync(n))if(c.readdirSync(n).length===0)h.log(`${I.cyan.bold(e.appName)} exists but is empty, continuing...`,h.ciaModifier(null));else{let o=await ne(new h.Select({promptText:I.redBright(`${I.bold("Warning:")} ${I.bold(e.appName)} already exists and isn't empty. How would you like to proceed?`),options:[{label:"Abort installation",value:"abort"},{label:"Clear the directory and continue installation",value:"clear"}],defaultValue:"abort",modifyOutput:h.ciaModifier()}));o==="abort"&&(h.log("Aborting installation..."),process.exit(1)),o==="clear"&&c.emptyDirSync(n)}let r=Ke({projectDir:n,baseTemplateName:e.base}),i=e.appName==="."?"App":I.hex("#EA570B").bold(e.appName);if(await ne(new h.Spinner({promise:r,workingText:"Scaffolding project files...",doneText:`Successfully scaffolded ${i}!`,errorText:"Error scaffolding project files",modifyOutput:h.ciaModifier(null)})),c.pathExistsSync(u.join(n,"pnpm-lock.yaml"))&&c.removeSync(u.join(n,"pnpm-lock.yaml")),c.pathExistsSync(u.join(n,"bun.lock"))&&c.removeSync(u.join(n,"bun.lock")),T(e.base)==="pnpm"&&e.base==="expo"&&await c.appendFile(u.join(n,".npmrc"),`node-linker=hoisted
12
+ enable-pre-post-scripts=true`),e.base==="expo"){let o=Le.default(t);V(u.join(n,"app.json"),'"name": "expo-template"',`"name": "${t}"`),V(u.join(n,"app.json"),'"slug": "expo-template"',`"slug": "${o}"`),V(u.join(n,"app/_layout.tsx"),'"My Instant App"',`"${t}"`)}return n},V=(e,t,n)=>{let i=c.readFileSync(e,"utf8").replaceAll(t,n);c.writeFileSync(e,i)},ze=async({projectDir:e,baseTemplateName:t})=>{let n=`instantdb/instant/examples/${t}`;await Ge(n,{mode:"tar",disableCache:!0}).clone(e)};async function Je(e,t){let n=We();n.add(".git");try{let r=u.join(e,".gitignore"),i=await c.readFile(r,"utf8");n.add(i)}catch{}await c.copy(e,t,{filter:r=>{let i=u.relative(e,r);return i===""?!0:!n.ignores(i)}})}var Ke=async({projectDir:e,baseTemplateName:t})=>{let n=u.join(O,`template/base/${t}`),r=!!process.env.INSTANT_CLI_DEV&&!!process.env.INSTANT_REPO_FOLDER,i=c.pathExistsSync(n),o=r?"dev":i?"bundled-template":"tiged";if(o==="bundled-template"){c.copySync(n,e);let s=u.join(e,"_gitignore"),a=u.join(e,".gitignore");c.pathExistsSync(s)&&c.renameSync(s,a);return}if(o==="dev"){let s=process.env.INSTANT_REPO_FOLDER;if(!s)throw new Error("INSTANT_REPO_FOLDER is required when using repo-examples scaffolding.");let a=u.join(s,"examples",t);await Je(a,e);return}process.env.INSTANT_CLI_DEV&&!process.env.INSTANT_REPO_FOLDER&&h.log(I.bold.yellowBright(`WARNING: INSTANT_CLI_DEV is TRUE but no INSTANT_REPO_FOLDER is set.
13
+ Using git to clone from main...`),h.ciaModifier(null)),await ze({projectDir:e,baseTemplateName:t})};import{execa as He}from"execa";import{renderUnwrap as Ye,UI as j}from"instant-cli/ui";var oe=async(e,t)=>{let n=He(e,["install"],{cwd:t}),r=await Ye(new j.Spinner({promise:n,workingText:`Installing dependencies with ${e}...`,doneText:"Successfully installed dependencies!",modifyOutput:j.ciaModifier(null)}));r.exitCode!==0&&(j.log(r.stderr,j.ciaModifier(null)),process.exit(1))};import At from"chalk";import qe from"fs-extra";import ae from"path";var Xe={claude:"CLAUDE.md",gemini:"GEMINI.md",cursor:"AGENTS.md",codex:"AGENTS.md",zed:"AGENTS.md",windsurf:"AGENTS.md"},ie=({projectDir:e,ruleFilesToAdd:t})=>{t!==null&&qe.copyFileSync(ae.join(O,"template/rules/AGENTS.md"),ae.join(e,Xe[t]))};import{execSync as G}from"child_process";import B from"path";import*as pe from"@clack/prompts";import R from"chalk";import{execa as _}from"execa";import le from"fs-extra";import{renderUnwrap as se,UI as k}from"instant-cli/ui";var Ze=e=>{try{return G("git --version",{cwd:e}),!0}catch{return!1}},Qe=e=>le.existsSync(B.join(e,".git")),et=async e=>{try{return await _("git",["rev-parse","--is-inside-work-tree"],{cwd:e,stdout:"ignore"}),!0}catch{return!1}},tt=()=>{let t=G("git --version").toString().trim().split(" ")[2],n=t?.split(".")[0],r=t?.split(".")[1];return{major:Number(n),minor:Number(r)}},nt=()=>G("git config --global init.defaultBranch || echo main").toString().trim(),ce=async e=>{if(!Ze(e)){pe.log.warn("Git is not installed. Skipping Git initialization.");return}let t=Qe(e),n=await et(e),r=B.parse(e).name;if(n&&t){if(!await se(new k.Confirmation({promptText:`${R.redBright.bold("Warning:")} Git is already initialized in "${r}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`,defaultValue:!1,modifyOutput:k.ciaModifier()})))return;le.removeSync(B.join(e,".git"))}else if(n&&!t&&!await se(new k.Confirmation({promptText:`${R.redBright.bold("Warning:")} "${r}" is already in a git worktree.
14
14
  Would you still like to initialize a new git repository in this directory?
15
- `,defaultValue:!1,modifyOutput:v.ciaModifier()})))return;try{let i=tt(),{major:o,minor:s}=et();o<2||o==2&&s<28?(await S("git",["init"],{cwd:e}),await S("git",["symbolic-ref","HEAD",`refs/heads/${i}`],{cwd:e})):await S("git",["init",`--initial-branch=${i}`],{cwd:e}),await S("git",["add","."],{cwd:e}),await S("git",["commit","-m","Initial commit (create-instant-app)"],{cwd:e}),v.log(M.dim(`${M.green("\u2713")} Git repository initialized successfully.`),v.ciaModifier(null))}catch{}};import rt from"env-paths";import{mkdir as ot,readFile as at,writeFile as it}from"node:fs/promises";import st from"open";import{join as pt}from"node:path";import{randomUUID as fe}from"node:crypto";import{version as nt}from"@instantdb/version";var ue=!!process.env.INSTANT_CLI_DEV,me=ue?"http://localhost:3000":"https://instantdb.com",z=process.env.INSTANT_CLI_API_URI||(ue?"http://localhost:8888":"https://api.instantdb.com");async function b({path:e,body:t,method:n="GET",authToken:r,metadata:i}){let s={"Content-Type":"application/json","X-Instant-Source":"create-instant-app","X-Instant-Version":nt,"X-Instant-Command":"create"};r&&(s.Authorization=`Bearer ${r}`),i&&(s["X-Instant-Metadata"]=JSON.stringify(i));let a=await fetch(`${z}${e}`,{method:n??"GET",headers:s,body:t?JSON.stringify(t):void 0,signal:AbortSignal.timeout(3e5)}),p;try{p=await a.json()}catch{p=null}if(!a.ok){let g=p.message||p.hint?.errors?.[0]?.message||"There was an error";throw new Error(g)}return p}import{renderUnwrap as E,UI as d}from"instant-cli/ui";function k(e){return e.replace(/[-_]/g," ").replace(/\b\w/g,t=>t.toUpperCase())}var lt=!!process.env.INSTANT_CLI_DEV,ct=!!process.env.INSTANT_CLI_FORCE_EPHEMERAL;function he(){let e=`instantdb-${lt?"dev":"prod"}`,{config:t}=rt(e);return{authConfigFilePath:pt(t,"a"),appConfigDirPath:t}}var ge=async(e,t,n,r)=>{let i=fe(),o=fe(),s={id:i,title:e,admin_token:o,org_id:n};return r?.rules&&(s.rules=r.rules),r?.schema&&(s.schema=r.schema),await b({method:"POST",authToken:t,path:"/dash/apps",body:s,metadata:r}),{appId:i,adminToken:o,source:"created"}},J=(e,t,n)=>{b({method:"POST",path:`/dash/apps/${e}/track-import`,authToken:t,metadata:n}).catch(()=>{})},we=async e=>await b({method:"GET",path:"/dash",authToken:e}),ye=async(e,t)=>await b({method:"GET",path:`/dash/orgs/${t}`,authToken:e});var dt=async(e,t)=>{try{let n=await we(e),r=n.apps.find(i=>i.id===t);if(r)return{appId:r.id,adminToken:r.admin_token};for(let i of n.orgs){let{apps:o}=await ye(e,i.id),s=o.find(a=>a.id===t);if(s)return{appId:s.id,adminToken:s.admin_token}}return null}catch{return null}},ut=async(e,t)=>{try{return await b({method:"GET",path:`/dash/apps/${e}/schema/pull`,authToken:t}),!0}catch{return!1}},mt=async()=>ct?null:process.env.INSTANT_CLI_AUTH_TOKEN?process.env.INSTANT_CLI_AUTH_TOKEN:await at(he().authConfigFilePath,"utf-8").catch(()=>null),$=async(e,t)=>{let n=await b({authToken:null,method:"POST",path:"/dash/apps/ephemeral",body:{title:e,rules:{$users:{view:"true"},$files:{allow:{view:"true",create:"true",delete:"true"}}}},metadata:t});return{appId:n.app.id,adminToken:n.app["admin-token"]}},Te=async(e,t,n)=>{let r=await mt();if(t?.yes&&!t?.app)if(r&&!t.temporary){let{appId:a,adminToken:p}=await ge(k(e),r,void 0,n);return{appId:a,adminToken:p,approach:"create"}}else{let{appId:a,adminToken:p}=await $(k(e),n);return{appId:a,adminToken:p,approach:"ephemeral"}}if(t?.temporary){let a=await E(new d.TextInput({defaultValue:k(e),prompt:"Enter a name for your temporary app:",placeholder:k(e),modifyOutput:d.ciaModifier()}));return{...await $(a,n),approach:"ephemeral"}}if(t?.app){if(t.token){if(!await ut(t.app,t.token))throw new Error("Invalid app ID and token combination. Please verify both the app ID and token are correct.");return d.log(`Linking to app: ${t.app}`,d.ciaModifier(null)),J(t.app,t.token,n),{appId:t.app,adminToken:t.token,approach:"import"}}if(r){let a=await dt(r,t.app);if(!a)throw new Error(`You don't have access to app "${t.app}". Please check the app ID or use --token to provide a token.`);return d.log(`Linking to app: ${t.app}`,d.ciaModifier(null)),J(a.appId,r,n),{appId:a.appId,adminToken:a.adminToken,approach:"import"}}throw new Error(`You must be logged in or provide --token when using --app. Either run 'npx instant-cli login' first, or use: --app ${t.app} --token <token>`)}if(!r){let a=await E(new d.Select({promptText:"You are not logged in.",options:[{label:"Login to your Instant account",value:"login"},{label:"Create a temporary app",value:"ephemeral"},{label:"Create an app later",value:"skip"}],modifyOutput:d.ciaModifier()}));if(a==="login"){let p=await b({authToken:null,method:"POST",path:"/dash/cli/auth/register"}).catch(Oe=>{throw new Error("Failed to register",{cause:Oe})}),{secret:g,ticket:h}=p;st(`${me}/dash?ticket=${h}`);let D=gt({secret:g}),K=await E(new d.Spinner({promise:D,workingText:"Waiting for login in browser",disappearWhenDone:!0,modifyOutput:d.ciaModifier(null)}));await ht(K.token),r=K.token}if(a==="skip")return d.log("Skipping app link step",d.ciaModifier(null)),null;if(a==="ephemeral"){let p=await E(new d.TextInput({defaultValue:k(e),prompt:"Enter a name for your temporary app:",placeholder:k(e),modifyOutput:d.ciaModifier()}));return{...await $(p,n),approach:"ephemeral"}}}if(!r)return null;let i=await we(r).catch(a=>{throw new Error("Failed to fetch dashboard",{cause:a})}),o=i.orgs.filter(a=>a.role!=="app-member");i.orgs=o;let s=await E(new d.AppSelector({startingMenuIndex:0,defaultAppName:k(e),allowCreate:!0,allowEphemeral:!0,api:{getDash(){return i},createEphemeralApp(a){return $(a,n)},getAppsForOrg:async a=>{let{apps:p}=await ye(r,a);return{apps:p}},createApp:async(a,p)=>{let{appId:g,adminToken:h}=await ge(a,r,p,n);return{appId:g,adminToken:h}}},modifyOutput:d.ciaModifier()}));return s?.approach==="import"&&J(s.appId,r,n),s};function ft(e){return new Promise(t=>setTimeout(t,e))}async function gt({secret:e}){for(let t=1;t<=120;t++){await ft(1e3);let n=await fetch(`${z}/dash/cli/auth/check`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:e})});if(!(!n.ok&&(await n.json()).hint.errors?.[0]?.issue==="waiting-for-user")&&n.ok)return n.json()}throw new Error("Timed out waiting for login")}async function ht(e){let t=he();return await ot(t.appConfigDirPath,{recursive:!0}),it(t.authConfigFilePath,e,"utf-8")}import wt from"fs-extra";import yt from"path";var Tt={"next-js-app-dir":"NEXT_PUBLIC_INSTANT_APP_ID","vite-react":"VITE_INSTANT_APP_ID","vite-vanilla":"VITE_INSTANT_APP_ID",expo:"EXPO_PUBLIC_INSTANT_APP_ID","tanstack-start":"VITE_INSTANT_APP_ID","bun-react":"BUN_PUBLIC_INSTANT_APP_ID","solidjs-vite":"VITE_INSTANT_APP_ID",sveltekit:"VITE_INSTANT_APP_ID","vue-vite":"VITE_INSTANT_APP_ID","tanstack-start-with-tanstack-query":"VITE_INSTANT_APP_ID","vercel-ai-sdk":"NEXT_PUBLIC_INSTANT_APP_ID","ai-chat":"NEXT_PUBLIC_INSTANT_APP_ID"},be=(e,t,n,r)=>{let i=yt.join(t,".env"),s=`${Tt[e.base]}=${n}
16
- INSTANT_APP_ADMIN_TOKEN=${r}`;wt.writeFileSync(i,s)};import{stdin as _,stdout as ke}from"process";import{setRawModeWindowsFriendly as Ie}from"instant-cli/ui";async function bt(){return new Promise(e=>{if(!ke.isTTY||!_.isTTY){e(null);return}let t=_.isRaw;t||Ie(_,!0);let n=setTimeout(()=>{i(),e(null)},100),r="",i=()=>{clearTimeout(n),_.removeListener("data",o),!t&&_.isTTY&&Ie(_,!1)},o=s=>{let a=s.toString();r+=a;let p=r.match(/\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)(?:\x1b\\|\x07)/);if(p){i();let[,g,h,D]=p;e(`rgb:${g}/${h}/${D}`)}};_.on("data",o),ke.write("\x1B]11;?\x1B\\")})}function kt(e){let t=e.match(/rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)/);if(!t)return null;let[,n,r,i]=t,o=Math.round(parseInt(n,16)/257),s=Math.round(parseInt(r,16)/257),a=Math.round(parseInt(i,16)/257);return{r:o,g:s,b:a}}function It(e){return(.299*e.r+.587*e.g+.114*e.b)/255>.5}async function _e(){try{let e=await bt();if(!e)return"unknown";let t=kt(e);return t?It(t)?"light":"dark":"unknown"}catch(e){return console.error("Error detecting terminal theme:",e),"unknown"}}import C from"chalk";var P=(e,t)=>e==="light"?C.bgYellowBright(t):C.bgBlackBright(t),xe=(e,t=console.log,n=!1)=>{let r=(process.stdout.columns||80)-4,i=e.split(`
17
- `),o=!0;i.forEach(s=>{if(s.length===0){o?(t(""),o=!1):console.log(C.gray("\u2502"));return}for(let a=0;a<s.length;a+=r){let p=s.slice(a,a+r);a+r<s.length&&p.length===r&&(p=p+"-"),o?(t(p),o=!1):console.log(C.gray("\u2502 ")+p)}}),n&&console.log(C.gray("\u2502"))};var O="\x1B[?25h";import{execa as Pt}from"execa";import ve from"fs-extra";import{permsTypescriptFileToCode as _t,schemaTypescriptFileToInstantSchema as xt}from"@instantdb/platform";function Se(e){for(let t of["src/instant.perms.ts","instant.perms.ts"])try{let n=ve.readFileSync(`${e}/${t}`,"utf8");return{code:_t(n,t)}}catch{}return null}function Pe(e){for(let t of["src/instant.schema.ts","instant.schema.ts"])try{let n=ve.readFileSync(`${e}/${t}`,"utf8");return xt(n,t)}catch{}return null}import{UI as A}from"instant-cli/ui";var Ae=e=>{if(e){if(e.approach==="import"){A.log(`Imported app with App ID: ${e.appId}`,A.ciaModifier());return}e.approach==="create"&&A.log(`Created new app with App ID: ${e.appId}`,A.ciaModifier()),e.approach==="ephemeral"&&A.log(`Created temporary app with App ID: ${e.appId}`,A.ciaModifier())}};var At=async()=>{process.argv.some(h=>["-h","--help","--version","-V"].includes(h))||ne();let e=await _e(),t=await ee(),[n,r]=X(t.appName),i=I(t.base),o=await ae(t,r);se({projectDir:o,ruleFilesToAdd:t.ruleFiles});let s={template:t.base,aiTool:t.ruleFiles??"none",usedAiPrompt:!!t.prompt,rules:Se(o),schema:Pe(o)},a=await Te(r==="."?n:r,t,s);Ae(a),a&&be(t,o,a.appId,a.adminToken);let p=Ee.readJSONSync(Ne.join(o,"package.json"));if(p.name=n,i!=="bun"){let{stdout:h}=await Pt(i,["-v"],{cwd:o});p.packageManager=`${i}@${h.trim()}`}Ee.writeJSONSync(Ne.join(o,"package.json"),p,{spaces:2}),await ie(I(t.base),o),t.createRepo&&await de(o),t.prompt&&(await H(t.prompt,o),process.stdout.write(O)),vt("Done!");let g=t.base==="expo"?"start":"dev";a?(console.log(`
15
+ `,defaultValue:!1,modifyOutput:k.ciaModifier()})))return;try{let i=nt(),{major:o,minor:s}=tt();o<2||o==2&&s<28?(await _("git",["init"],{cwd:e}),await _("git",["symbolic-ref","HEAD",`refs/heads/${i}`],{cwd:e})):await _("git",["init",`--initial-branch=${i}`],{cwd:e}),await _("git",["add","."],{cwd:e}),await _("git",["commit","-m","Initial commit (create-instant-app)"],{cwd:e}),k.log(R.dim(`${R.green("\u2713")} Git repository initialized successfully.`),k.ciaModifier(null))}catch{}};import ot from"env-paths";import{mkdir as at,readFile as it,writeFile as st}from"node:fs/promises";import pt from"open";import{join as lt}from"node:path";import{randomUUID as me}from"node:crypto";import{version as rt}from"@instantdb/version";var de=!!process.env.INSTANT_CLI_DEV,ue=de?"http://localhost:3000":"https://instantdb.com",L=process.env.INSTANT_CLI_API_URI||(de?"http://localhost:8888":"https://api.instantdb.com");async function w({path:e,body:t,method:n="GET",authToken:r,metadata:i}){let s={"Content-Type":"application/json","X-Instant-Source":"create-instant-app","X-Instant-Version":rt,"X-Instant-Command":"create"};r&&(s.Authorization=`Bearer ${r}`),i&&(s["X-Instant-Metadata"]=JSON.stringify(i));let a=await fetch(`${L}${e}`,{method:n??"GET",headers:s,body:t?JSON.stringify(t):void 0,signal:AbortSignal.timeout(3e5)}),p;try{p=await a.json()}catch{p=null}if(!a.ok){let f=p.message||p.hint?.errors?.[0]?.message||"There was an error";throw new Error(f)}return p}import{renderUnwrap as A,UI as d}from"instant-cli/ui";function y(e){return e.replace(/[-_]/g," ").replace(/\b\w/g,t=>t.toUpperCase())}var ct=!!process.env.INSTANT_CLI_DEV,dt=!!process.env.INSTANT_CLI_FORCE_EPHEMERAL;function ge(){let e=`instantdb-${ct?"dev":"prod"}`,{config:t}=ot(e);return{authConfigFilePath:lt(t,"a"),appConfigDirPath:t}}var fe=async(e,t,n,r)=>{let i=me(),o=me(),s={id:i,title:e,admin_token:o,org_id:n};return r?.rules&&(s.rules=r.rules),r?.schema&&(s.schema=r.schema),await w({method:"POST",authToken:t,path:"/dash/apps",body:s,metadata:r}),{appId:i,adminToken:o,source:"created"}},W=(e,t,n)=>{w({method:"POST",path:`/dash/apps/${e}/track-import`,authToken:t,metadata:n}).catch(()=>{})},he=async e=>await w({method:"GET",path:"/dash",authToken:e}),we=async(e,t)=>await w({method:"GET",path:`/dash/orgs/${t}`,authToken:e});var ut=async(e,t)=>{try{let n=await he(e),r=n.apps.find(i=>i.id===t);if(r)return{appId:r.id,adminToken:r.admin_token};for(let i of n.orgs){let{apps:o}=await we(e,i.id),s=o.find(a=>a.id===t);if(s)return{appId:s.id,adminToken:s.admin_token}}return null}catch{return null}},mt=async(e,t)=>{try{return await w({method:"GET",path:`/dash/apps/${e}/schema/pull`,authToken:t}),!0}catch{return!1}},ft=async()=>dt?null:process.env.INSTANT_CLI_AUTH_TOKEN?process.env.INSTANT_CLI_AUTH_TOKEN:await it(ge().authConfigFilePath,"utf-8").catch(()=>null),F=async(e,t)=>{let n=await w({authToken:null,method:"POST",path:"/dash/apps/ephemeral",body:{title:e,rules:{$users:{view:"true"},$files:{allow:{view:"true",create:"true",delete:"true"}}}},metadata:t});return{appId:n.app.id,adminToken:n.app["admin-token"]}},ye=async(e,t,n)=>{let r=await ft();if(t?.yes&&!t?.app)if(r&&!t.temporary){let{appId:a,adminToken:p}=await fe(y(e),r,void 0,n);return{appId:a,adminToken:p,approach:"create"}}else{let{appId:a,adminToken:p}=await F(y(e),n);return{appId:a,adminToken:p,approach:"ephemeral"}}if(t?.temporary){let a=await A(new d.TextInput({defaultValue:y(e),prompt:"Enter a name for your temporary app:",placeholder:y(e),modifyOutput:d.ciaModifier()}));return{...await F(a,n),approach:"ephemeral"}}if(t?.app){if(t.token){if(!await mt(t.app,t.token))throw new Error("Invalid app ID and token combination. Please verify both the app ID and token are correct.");return d.log(`Linking to app: ${t.app}`,d.ciaModifier(null)),W(t.app,t.token,n),{appId:t.app,adminToken:t.token,approach:"import"}}if(r){let a=await ut(r,t.app);if(!a)throw new Error(`You don't have access to app "${t.app}". Please check the app ID or use --token to provide a token.`);return d.log(`Linking to app: ${t.app}`,d.ciaModifier(null)),W(a.appId,r,n),{appId:a.appId,adminToken:a.adminToken,approach:"import"}}throw new Error(`You must be logged in or provide --token when using --app. Either run 'npx instant-cli login' first, or use: --app ${t.app} --token <token>`)}if(!r){let a=await A(new d.Select({promptText:"You are not logged in.",options:[{label:"Login to your Instant account",value:"login"},{label:"Create a temporary app",value:"ephemeral"},{label:"Create an app later",value:"skip"}],modifyOutput:d.ciaModifier()}));if(a==="login"){let p=await w({authToken:null,method:"POST",path:"/dash/cli/auth/register"}).catch(Ce=>{throw new Error("Failed to register",{cause:Ce})}),{secret:f,ticket:g}=p;pt(`${ue}/dash?ticket=${g}`);let M=ht({secret:f}),z=await A(new d.Spinner({promise:M,workingText:"Waiting for login in browser",disappearWhenDone:!0,modifyOutput:d.ciaModifier(null)}));await wt(z.token),r=z.token}if(a==="skip")return d.log("Skipping app link step",d.ciaModifier(null)),null;if(a==="ephemeral"){let p=await A(new d.TextInput({defaultValue:y(e),prompt:"Enter a name for your temporary app:",placeholder:y(e),modifyOutput:d.ciaModifier()}));return{...await F(p,n),approach:"ephemeral"}}}if(!r)return null;let i=await he(r).catch(a=>{throw new Error("Failed to fetch dashboard",{cause:a})}),o=i.orgs.filter(a=>a.role!=="app-member");i.orgs=o;let s=await A(new d.AppSelector({startingMenuIndex:0,defaultAppName:y(e),allowCreate:!0,allowEphemeral:!0,api:{getDash(){return i},createEphemeralApp(a){return F(a,n)},getAppsForOrg:async a=>{let{apps:p}=await we(r,a);return{apps:p}},createApp:async(a,p)=>{let{appId:f,adminToken:g}=await fe(a,r,p,n);return{appId:f,adminToken:g}}},modifyOutput:d.ciaModifier()}));return s?.approach==="import"&&W(s.appId,r,n),s};function gt(e){return new Promise(t=>setTimeout(t,e))}async function ht({secret:e}){for(let t=1;t<=120;t++){await gt(1e3);let n=await fetch(`${L}/dash/cli/auth/check`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({secret:e})});if(!(!n.ok&&(await n.json()).hint.errors?.[0]?.issue==="waiting-for-user")&&n.ok)return n.json()}throw new Error("Timed out waiting for login")}async function wt(e){let t=ge();return await at(t.appConfigDirPath,{recursive:!0}),st(t.authConfigFilePath,e,"utf-8")}import yt from"fs-extra";import Tt from"path";var bt={"next-js-app-dir":"NEXT_PUBLIC_INSTANT_APP_ID","vite-react":"VITE_INSTANT_APP_ID","vite-vanilla":"VITE_INSTANT_APP_ID",expo:"EXPO_PUBLIC_INSTANT_APP_ID","tanstack-start":"VITE_INSTANT_APP_ID","bun-react":"BUN_PUBLIC_INSTANT_APP_ID","solidjs-vite":"VITE_INSTANT_APP_ID",sveltekit:"VITE_INSTANT_APP_ID","vue-vite":"VITE_INSTANT_APP_ID","tanstack-start-with-tanstack-query":"VITE_INSTANT_APP_ID","vercel-ai-sdk":"NEXT_PUBLIC_INSTANT_APP_ID","ai-chat":"NEXT_PUBLIC_INSTANT_APP_ID"},Te=(e,t,n,r)=>{let i=Tt.join(t,".env"),s=`${bt[e.base]}=${n}
16
+ INSTANT_APP_ADMIN_TOKEN=${r}`;yt.writeFileSync(i,s)};import{stdin as b,stdout as be}from"process";import{setRawModeWindowsFriendly as Ie}from"instant-cli/ui";async function It(){return new Promise(e=>{if(!be.isTTY||!b.isTTY){e(null);return}let t=b.isRaw;t||Ie(b,!0);let n=setTimeout(()=>{i(),e(null)},100),r="",i=()=>{clearTimeout(n),b.removeListener("data",o),!t&&b.isTTY&&Ie(b,!1)},o=s=>{let a=s.toString();r+=a;let p=r.match(/\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)(?:\x1b\\|\x07)/);if(p){i();let[,f,g,M]=p;e(`rgb:${f}/${g}/${M}`)}};b.on("data",o),be.write("\x1B]11;?\x1B\\")})}function kt(e){let t=e.match(/rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)/);if(!t)return null;let[,n,r,i]=t,o=Math.round(parseInt(n,16)/257),s=Math.round(parseInt(r,16)/257),a=Math.round(parseInt(i,16)/257);return{r:o,g:s,b:a}}function _t(e){return(.299*e.r+.587*e.g+.114*e.b)/255>.5}async function ke(){try{let e=await It();if(!e)return"unknown";let t=kt(e);return t?_t(t)?"light":"dark":"unknown"}catch(e){return console.error("Error detecting terminal theme:",e),"unknown"}}import S from"chalk";var x=(e,t)=>e==="light"?S.bgYellowBright(t):S.bgBlackBright(t),_e=(e,t=console.log,n=!1)=>{let r=(process.stdout.columns||80)-4,i=e.split(`
17
+ `),o=!0;i.forEach(s=>{if(s.length===0){o?(t(""),o=!1):console.log(S.gray("\u2502"));return}for(let a=0;a<s.length;a+=r){let p=s.slice(a,a+r);a+r<s.length&&p.length===r&&(p=p+"-"),o?(t(p),o=!1):console.log(S.gray("\u2502 ")+p)}}),n&&console.log(S.gray("\u2502"))};var N="\x1B[?25h";import{execa as St}from"execa";import xe from"fs-extra";import{permsTypescriptFileToCode as xt,schemaTypescriptFileToInstantSchema as vt}from"@instantdb/platform";function ve(e){for(let t of["src/instant.perms.ts","instant.perms.ts"])try{let n=xe.readFileSync(`${e}/${t}`,"utf8");return{code:xt(n,t)}}catch{}return null}function Pe(e){for(let t of["src/instant.schema.ts","instant.schema.ts"])try{let n=xe.readFileSync(`${e}/${t}`,"utf8");return vt(n,t)}catch{}return null}import{UI as v}from"instant-cli/ui";var Ae=e=>{if(e){if(e.approach==="import"){v.log(`Imported app with App ID: ${e.appId}`,v.ciaModifier());return}e.approach==="create"&&v.log(`Created new app with App ID: ${e.appId}`,v.ciaModifier()),e.approach==="ephemeral"&&v.log(`Created temporary app with App ID: ${e.appId}`,v.ciaModifier())}};var Nt=async()=>{process.argv.some(g=>["-h","--help","--version","-V"].includes(g))||ee();let e=await ke(),t=await Z(),[n,r]=Y(t.appName),i=T(t.base),o=await re(t,r);ie({projectDir:o,ruleFilesToAdd:t.ruleFiles});let s={template:t.base,aiTool:t.ruleFiles??"none",usedAiPrompt:!!t.prompt,rules:ve(o),schema:Pe(o)},a=await ye(r==="."?n:r,t,s);Ae(a),a&&Te(t,o,a.appId,a.adminToken);let p=Ne.readJSONSync(Se.join(o,"package.json"));if(p.name=n,i!=="bun"){let{stdout:g}=await St(i,["-v"],{cwd:o});p.packageManager=`${i}@${g.trim()}`}Ne.writeJSONSync(Se.join(o,"package.json"),p,{spaces:2}),await oe(T(t.base),o),t.createRepo&&await ce(o),t.prompt&&(await J(t.prompt,o),process.stdout.write(N)),Pt("Done!");let f=t.base==="expo"?"start":"dev";a?(console.log(`
18
18
  \u{1F389} Success! Your project is ready to go!
19
19
 
20
20
  To get started:
21
- 1. ${P(e,"cd "+r)}
22
- 2. ${P(e,I(t.base)+" run "+g)}
21
+ 1. ${x(e,"cd "+r)}
22
+ 2. ${x(e,T(t.base)+" run "+f)}
23
23
  `),a.approach==="ephemeral"&&console.log(`
24
24
  An ephemeral app has been created and added to your .env file.
25
- It will expire in two weeks. For a permanent app, sign in and use ${P(e,"npx instant-cli claim")}
25
+ It will expire in two weeks. For a permanent app, sign in and use ${x(e,"npx instant-cli claim")}
26
26
  `)):console.log(`
27
27
  \u{1F389} Success! Your project is ready to go!
28
28
 
29
29
  To get started:
30
- 1. ${P(e,"cd "+r)}
31
- 2. Create a new app on ${St.underline("www.instantdb.com")}
30
+ 1. ${x(e,"cd "+r)}
31
+ 2. Create a new app on ${At.underline("www.instantdb.com")}
32
32
  3. Add your APP_ID to the .env file
33
- 4. ${P(e,I(t.base)+" run "+g)}
34
- `),process.stdout.write(O),process.exit(0)};At().catch(e=>{Ce.error("Aborting installation..."),xe(e.message,Ce.error),process.stdout.write(O),process.exit(1)});process.on("SIGINT",()=>{process.stdout.write(O),process.exit(0)});
33
+ 4. ${x(e,T(t.base)+" run "+f)}
34
+ `),process.stdout.write(N),process.exit(0)};Nt().catch(e=>{Ee.error("Aborting installation..."),_e(e.message,Ee.error),process.stdout.write(N),process.exit(1)});process.on("SIGINT",()=>{process.stdout.write(N),process.exit(0)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-instant-app",
3
- "version": "1.0.39",
3
+ "version": "1.0.40",
4
4
  "description": "Scaffold a new web/mobile app with InstantDB",
5
5
  "homepage": "https://github.com/instantdb/instant/tree/main/client/packages/create-instant-app",
6
6
  "repository": {
@@ -35,9 +35,9 @@
35
35
  "slugify": "^1.6.6",
36
36
  "sort-package-json": "^2.10.0",
37
37
  "tiged": "^2.12.7",
38
- "@instantdb/platform": "1.0.39",
39
- "@instantdb/version": "1.0.39",
40
- "instant-cli": "1.0.39"
38
+ "@instantdb/version": "1.0.40",
39
+ "@instantdb/platform": "1.0.40",
40
+ "instant-cli": "1.0.40"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@anthropic-ai/sdk": "^0.60.0",
@@ -344,9 +344,15 @@ import { clientDb } from '@/lib/clientDb';
344
344
  // For react/react-native apps use db.useAuth
345
345
  function App() {
346
346
  const { isLoading, user, error } = clientDb.useAuth();
347
- if (isLoading) { return null; }
348
- if (error) { return <Error message={error.message} />; }
349
- if (user) { return <Main />; }
347
+ if (isLoading) {
348
+ return null;
349
+ }
350
+ if (error) {
351
+ return <Error message={error.message} />;
352
+ }
353
+ if (user) {
354
+ return <Main />;
355
+ }
350
356
  return <Login />;
351
357
  }
352
358
 
@@ -354,9 +360,13 @@ function App() {
354
360
  function App() {
355
361
  renderLoading();
356
362
  db.subscribeAuth((auth) => {
357
- if (auth.error) { renderAuthError(auth.error.message); }
358
- else if (auth.user) { renderLoggedInPage(auth.user); }
359
- else { renderSignInPage(); }
363
+ if (auth.error) {
364
+ renderAuthError(auth.error.message);
365
+ } else if (auth.user) {
366
+ renderLoggedInPage(auth.user);
367
+ } else {
368
+ renderSignInPage();
369
+ }
360
370
  });
361
371
  }
362
372
  ```
@@ -1,432 +0,0 @@
1
- ---
2
- description:
3
- globs:
4
- alwaysApply: true
5
- ---
6
-
7
- Act as a world-class senior frontend engineer with deep expertise in InstantDB
8
- and UI/UX design. Your primary goal is to generate complete and functional apps
9
- with excellent visual aesthetics using InstantDB as the backend.
10
-
11
- # About InstantDB aka Instant
12
-
13
- Instant is a client-side database (Modern Firebase) with built-in queries, transactions, auth, permissions, storage, real-time, and offline support.
14
-
15
- # Instant SDKs
16
-
17
- Instant provides client-side JS SDKs and an admin SDK:
18
-
19
- - `@instantdb/core` --- vanilla JS
20
- - `@instantdb/react` --- React
21
- - `@instantdb/react-native` --- React Native / Expo
22
- - `@instantdb/solidjs` --- SolidJS
23
- - `@instantdb/svelte` --- Svelte
24
- - `@instantdb/vue` --- Vue
25
- - `@instantdb/admin` --- backend scripts / servers
26
-
27
- When installing, always check what package manager the project uses (npm, pnpm,
28
- bun) first and then install the latest version of the Instant SDK. If working in
29
- React use Next and Tailwind unless specified otherwise.
30
-
31
- # Managing Instant Apps
32
-
33
- ## Prerequisites
34
-
35
- Look for `instant.schema.ts` and `instant.perms.ts`. These define the schema and permissions.
36
- Look for an app id and admin token in `.env` or another env file.
37
-
38
- If schema/perm files exist but the app id/admin token are missing, ask the user where to find them or whether to create a new app.
39
-
40
- To create a new app:
41
-
42
- ```bash
43
- npx instant-cli init-without-files --title <APP_NAME>
44
- ```
45
-
46
- This outputs an app id and admin token. Store them in an env file.
47
-
48
- If you get an error related to not being logged in tell the user to:
49
-
50
- - Sign up for free or log in at https://instantdb.com
51
- - Then run `npx instant-cli login` to authenticate the CLI
52
- - Then re-run the init command
53
-
54
- If you have an app id/admin token but no schema/perm files, pull them:
55
-
56
- ```bash
57
- npx instant-cli pull --yes
58
- ```
59
-
60
- ## Schema changes
61
-
62
- Edit `instant.schema.ts`, then push:
63
-
64
- ```bash
65
- npx instant-cli push schema --yes
66
- ```
67
-
68
- New fields = additions; missing fields = deletions.
69
-
70
- To rename fields:
71
-
72
- ```bash
73
- npx instant-cli push schema --rename 'posts.author:posts.creator stores.owner:stores.manager' --yes
74
- ```
75
-
76
- ## Permission changes
77
-
78
- Edit `instant.perms.ts`, then push:
79
-
80
- ```bash
81
- npx instant-cli push perms --yes
82
- ```
83
-
84
- # CRITICAL Query Guidelines
85
-
86
- CRITICAL: When using React make sure to follow the rules of hooks. Remember, you can't have hooks show up conditionally.
87
-
88
- CRITICAL: You MUST index any field you want to filter or order by in the schema. If you do not, you will get an error when you try to filter or order by it.
89
-
90
- Here is how ordering works:
91
-
92
- ```text
93
- Ordering: order: { field: 'asc' | 'desc' }
94
-
95
- Example: $: { order: { dueDate: 'asc' } }
96
-
97
- Notes: - Field must be indexed + typed in schema
98
- - Cannot order by nested attributes (e.g. 'owner.name')
99
- ```
100
-
101
- CRITICAL: Here is a concise summary of the `where` operator map which defines all the filtering options you can use with InstantDB queries to narrow results based on field values, comparisons, arrays, text patterns, and logical conditions.
102
-
103
- ```text
104
- Equality: { field: value }
105
-
106
- Inequality: { field: { $ne: value } }
107
-
108
- Null checks: { field: { $isNull: true | false } }
109
-
110
- Comparison: $gt, $lt, $gte, $lte (indexed + typed fields only)
111
-
112
- Sets: { field: { $in: [v1, v2] } }
113
-
114
- Substring: { field: { $like: 'Get%' } } // case-sensitive
115
- { field: { $ilike: '%get%' } } // case-insensitive
116
-
117
- Logic: and: [ {...}, {...} ]
118
- or: [ {...}, {...} ]
119
-
120
- Nested fields: 'relation.field': value
121
- ```
122
-
123
- CRITICAL: The operator map above is the full set of `where` filters Instant
124
- supports right now. There is no `$exists`, `$nin`, or `$regex`. And `$like` and
125
- `$ilike` are what you use for `startsWith` / `endsWith` / `includes`.
126
-
127
- CRITICAL: Pagination keys (`limit`, `offset`, `first`, `after`, `last`, `before`) only work on top-level namespaces. DO NOT use them on nested relations or else you will get an error.
128
-
129
- CRITICAL: If you are unsure how something works in InstantDB you fetch the relevant urls in the documentation to learn more.
130
-
131
- # CRITICAL Permission Guidelines
132
-
133
- Below are some CRITICAL guidelines for writing permissions in InstantDB.
134
-
135
- ## `data.ref`
136
-
137
- - Use `data.ref("<path.to.attr>")` for linked attributes.
138
- - Always returns a **list**.
139
- - Must end with an **attribute**.
140
-
141
- **Correct**
142
-
143
- ```cel
144
- auth.id in data.ref('post.author.id') // auth.id in list of author ids
145
- data.ref('owner.id') == [] // there is no owner
146
- ```
147
-
148
- **Errors**
149
-
150
- ```cel
151
- auth.id in data.post.author.id
152
- auth.id in data.ref('author')
153
- data.ref('admins.id') == auth.id
154
- auth.id == data.ref('owner.id')
155
- data.ref('owner.id') == null
156
- data.ref('owner.id').length > 0
157
- ```
158
-
159
- ## `auth.ref`
160
-
161
- - Same as `data.ref` but path must start with `$user`.
162
- - Returns a list.
163
-
164
- **Correct**
165
-
166
- ```cel
167
- 'admin' in auth.ref('$user.role.type')
168
- auth.ref('$user.role.type')[0] == 'admin'
169
- ```
170
-
171
- **Errors**
172
-
173
- ```cel
174
- auth.ref('role.type')
175
- auth.ref('$user.role.type') == 'admin'
176
- ```
177
-
178
- ## Unsupported
179
-
180
- ```cel
181
- newData.ref('x')
182
- data.ref(someVar + '.members.id')
183
- ```
184
-
185
- ## $users Permissions
186
-
187
- - Default `view` permission is `auth.id == data.id`
188
- - Default `update` and `delete` permissions is false
189
- - Default `create` permission is true (anyone can sign up)
190
- - Can override `view`, `update`, and `create`
191
- - Cannot override `delete`
192
- - The `create` rule runs during auth signup flows (not via `transact`). Use it to restrict signups or validate `extraFields`.
193
- - `extraFields` require an explicit `create` rule. Without one, signup is blocked to prevent unvalidated writes.
194
-
195
- ## $files Permissions
196
-
197
- - Default permissions are all false. Override as needed to allow access.
198
- - `data.ref` does not work for `$files` permissions.
199
- - Use `data.path.startsWith(...)` or `data.path.endsWith(...)` to write
200
- path-based rules.
201
-
202
- ## Field-level Permissions
203
-
204
- Restrict access to specific fields while keeping the entity public:
205
-
206
- ```json
207
- {
208
- "$users": {
209
- "allow": {
210
- "view": "true"
211
- },
212
- "fields": {
213
- "email": "auth.id == data.id"
214
- }
215
- }
216
- }
217
- ```
218
-
219
- Notes:
220
-
221
- - Field rules override entity-level `view` for that field
222
- - Useful for hiding sensitive data (emails, phone numbers) on public entities
223
-
224
- # CRITICAL Storage Guidelines
225
-
226
- CRITICAL: If an app displays images or files, use Instant Storage. Do not store
227
- URLs as string attributes on your entities. This includes seed scripts: do not
228
- use placeholder image URLs (e.g. picsum.photos) as string attributes to fake
229
- file support.
230
-
231
- Uploads auto-create `$files` entities. Link them to your data via the schema,
232
- then query through the relationship to get URLs.
233
-
234
- CRITICAL: You MUST include `$files` in your schema entities if you use Storage.
235
-
236
- CRITICAL: `$files` entities can only be created via `db.storage.uploadFile`. You
237
- cannot create `$files` via `db.transact`, and you cannot set `url` via transactions.
238
-
239
- ```tsx
240
- entities: {
241
- $files: i.entity({
242
- path: i.string().unique().indexed(),
243
- url: i.string(),
244
- }),
245
- posts: i.entity({
246
- caption: i.string(),
247
- }),
248
- },
249
- links: {
250
- postImage: {
251
- forward: { on: "posts", has: "one", label: "image" },
252
- reverse: { on: "$files", has: "many", label: "posts" },
253
- },
254
- }
255
-
256
- // Upload and link the returned file ID to your entity
257
- const postId = id();
258
- const { data } = await db.storage.uploadFile(`posts/${postId}/${file.name}`, file);
259
- db.transact(
260
- db.tx.posts[postId].update({ caption }).link({ image: data.id })
261
- );
262
-
263
- // Query through the relationship to get the URL
264
- const { data } = db.useQuery({ posts: { image: {} } });
265
- <img src={post.image.url} />
266
- ```
267
-
268
- # CRITICAL Rooms Guidelines
269
-
270
- CRITICAL: Hooks for presence and topics live on `db.rooms` and take the room as the first arg. The room object itself has no `usePresence` or `publishPresence` methods.
271
-
272
- Rooms host two ephemeral primitives: presence (cursor positions, who's online) and topics (live reactions). Use them only for data that should NOT persist. Persisted data via `transact` already syncs in real-time to all subscribed clients, so reach for rooms only when the data is intentionally ephemeral.
273
-
274
- ## Presence
275
-
276
- Each peer publishes a presence object readable by all other peers in the room. Retained for the connection and cleaned up automatically on disconnect.
277
-
278
- ```tsx
279
- const room = db.room('chat', 'main');
280
- const { user, peers, publishPresence } = db.rooms.usePresence(room, {
281
- initialPresence: { x: 0, y: 0 },
282
- });
283
- // peers is keyed by peerId, not an array. Use Object.values(peers) to iterate
284
- publishPresence({ x: 50, y: 50 });
285
- ```
286
-
287
- ## Topics
288
-
289
- Topic payloads aren't retained. Peers only see events fired while they're listening.
290
-
291
- ```tsx
292
- const room = db.room('chat', 'main');
293
-
294
- const publishEmoji = db.rooms.usePublishTopic(room, 'emoji');
295
- publishEmoji({ name: 'fire' });
296
-
297
- db.rooms.useTopicEffect(room, 'emoji', (payload) => {
298
- animateEmoji(payload.name);
299
- });
300
- ```
301
-
302
- # Best Practices
303
-
304
- ## Pass `schema` when initializing Instant
305
-
306
- Always pass `schema` when initializing Instant to get type safety for queries and transactions
307
-
308
- ```tsx
309
- import schema from '@/instant.schema';
310
-
311
- // On client
312
- import { init } from '@instantdb/react'; // or your relevant Instant SDK
313
- const clientDb = init({ appId, schema });
314
-
315
- // On backend
316
- import { init } from '@instantdb/admin';
317
- const adminDb = init({ appId, adminToken, schema });
318
- ```
319
-
320
- ## Use `id()` to generate ids
321
-
322
- Always use `id()` to generate ids for new entities
323
-
324
- ```tsx
325
- import { id } from '@instantdb/react'; // or your relevant Instant SDK
326
- import { clientDb } from '@/lib/clientDb';
327
- clientDb.transact(clientDb.tx.todos[id()].create({ title: 'New Todo' }));
328
- ```
329
-
330
- ## Use Instant utility types for data models
331
-
332
- Always use Instant utility types to type data models
333
-
334
- ```tsx
335
- import { AppSchema } from '@/instant.schema';
336
-
337
- type Todo = InstaQLEntity<AppSchema, 'todos'>; // todo from clientDb.useQuery({ todos: {} })
338
- type PostsWithProfile = InstaQLEntity<
339
- AppSchema,
340
- 'posts',
341
- { author: { avatar: {} } }
342
- >; // post from clientDb.useQuery({ posts: { author: { avatar: {} } } })
343
- ```
344
-
345
- ## Use `db.useAuth` or `db.subscribeAuth` for auth state
346
-
347
- ```tsx
348
- import { clientDb } from '@/lib/clientDb';
349
-
350
- // For react/react-native apps use db.useAuth
351
- function App() {
352
- const { isLoading, user, error } = clientDb.useAuth();
353
- if (isLoading) { return null; }
354
- if (error) { return <Error message={error.message} />; }
355
- if (user) { return <Main />; }
356
- return <Login />;
357
- }
358
-
359
- // For vanilla JS apps use db.subscribeAuth
360
- function App() {
361
- renderLoading();
362
- db.subscribeAuth((auth) => {
363
- if (auth.error) { renderAuthError(auth.error.message); }
364
- else if (auth.user) { renderLoggedInPage(auth.user); }
365
- else { renderSignInPage(); }
366
- });
367
- }
368
- ```
369
-
370
- ## Set custom properties at signup with `extraFields`
371
-
372
- Pass `extraFields` to any sign-in method to write custom `$users` properties atomically on user creation.
373
- Fields must be defined as optional attrs on `$users` in your schema.
374
- Use the `created` boolean to scaffold data for new users.
375
-
376
- ```tsx
377
- // Set properties at signup
378
- const { user, created } = await db.auth.signInWithMagicCode({
379
- email,
380
- code,
381
- extraFields: { nickname, createdAt: Date.now() },
382
- });
383
-
384
- // Scaffold data for new users
385
- if (created) {
386
- db.transact([
387
- db.tx.settings[id()]
388
- .update({ theme: 'light', notifications: true })
389
- .link({ user: user.id }),
390
- ]);
391
- }
392
- ```
393
-
394
- # Ad-hoc queries from the CLI
395
-
396
- Run `npx instant-cli query '{ posts: {} }' --admin` to query your app. A context flag is required: `--admin`, `--as-email <email>`, or `--as-guest`. Also supports `--app <id>`.
397
-
398
- # Instant Documentation
399
-
400
- The bullets below are links to the Instant documentation. They provide detailed information on how to use different features of InstantDB. Each line follows the pattern of
401
-
402
- - [TOPIC](URL): Description of the topic.
403
-
404
- Fetch the URL for a topic to learn more about it.
405
-
406
- - [Common mistakes](https://www.instantdb.com/docs/common-mistakes.md): Common mistakes when working with Instant
407
- - [Initializing Instant](https://www.instantdb.com/docs/init.md): How to integrate Instant with your app.
408
- - [Modeling data](https://www.instantdb.com/docs/modeling-data.md): How to model data with Instant's schema.
409
- - [Writing data](https://www.instantdb.com/docs/instaml.md): How to write data with Instant using InstaML.
410
- - [Reading data](https://www.instantdb.com/docs/instaql.md): How to read data with Instant using InstaQL.
411
- - [Instant on the Backend](https://www.instantdb.com/docs/backend.md): How to use Instant on the server with the Admin SDK.
412
- - [Patterns](https://www.instantdb.com/docs/patterns.md): Common patterns for working with InstantDB.
413
- - [Auth](https://www.instantdb.com/docs/auth/magic-codes.md): How to add magic code auth to your Instant app.
414
- - [Guest Auth](https://www.instantdb.com/docs/auth/guest-auth.md): How to add guest auth to your Instant app.
415
- - [Other Auth](https://www.instantdb.com/docs/auth.md): Additional auth methods supported by Instant.
416
- - [Managing users](https://www.instantdb.com/docs/users.md): How to manage users in your Instant app.
417
- - [Presence, Cursors, and Activity](https://www.instantdb.com/docs/presence-and-topics.md): How to add ephemeral features like presence and cursors to your Instant app.
418
- - [Instant CLI](https://www.instantdb.com/docs/cli.md): How to use the Instant CLI to manage schema.
419
- - [Storage](https://www.instantdb.com/docs/storage.md): How to upload and serve files with Instant.
420
- - [Streams](https://www.instantdb.com/docs/streams.md): How to use streams with Instant.
421
- - [Stripe Payments](https://www.instantdb.com/docs/stripe-payments.md): How to integrate Stripe payments with Instant.
422
- - [React Native](https://www.instantdb.com/docs/start-rn.md): How to use Instant in React Native apps.
423
- - [Vanilla JS](https://www.instantdb.com/docs/start-vanilla.md): How to use Instant in vanilla JS apps.
424
- - [SolidJS](https://www.instantdb.com/docs/start-solidjs.md): How to use Instant in SolidJS apps.
425
- - [Svelte](https://www.instantdb.com/docs/start-svelte.md): How to use Instant in Svelte apps.
426
- - [Vue](https://www.instantdb.com/docs/start-vue.md): How to use Instant in Vue apps.
427
- - [TanStack](https://www.instantdb.com/docs/start-tanstack.md): How to use Instant in TanStack apps.
428
-
429
- # Final Note
430
-
431
- Think before you answer. Make sure your code passes typechecks `tsc --noEmit` and works as expected.
432
- Remember! AESTHETICS ARE VERY IMPORTANT. All apps should LOOK AMAZING and have GREAT FUNCTIONALITY!
@@ -1,432 +0,0 @@
1
- ---
2
- trigger: always_on
3
- description: How to use InstantDB
4
- globs: ['**/*.tsx', '**/*.ts']
5
- ---
6
-
7
- Act as a world-class senior frontend engineer with deep expertise in InstantDB
8
- and UI/UX design. Your primary goal is to generate complete and functional apps
9
- with excellent visual aesthetics using InstantDB as the backend.
10
-
11
- # About InstantDB aka Instant
12
-
13
- Instant is a client-side database (Modern Firebase) with built-in queries, transactions, auth, permissions, storage, real-time, and offline support.
14
-
15
- # Instant SDKs
16
-
17
- Instant provides client-side JS SDKs and an admin SDK:
18
-
19
- - `@instantdb/core` --- vanilla JS
20
- - `@instantdb/react` --- React
21
- - `@instantdb/react-native` --- React Native / Expo
22
- - `@instantdb/solidjs` --- SolidJS
23
- - `@instantdb/svelte` --- Svelte
24
- - `@instantdb/vue` --- Vue
25
- - `@instantdb/admin` --- backend scripts / servers
26
-
27
- When installing, always check what package manager the project uses (npm, pnpm,
28
- bun) first and then install the latest version of the Instant SDK. If working in
29
- React use Next and Tailwind unless specified otherwise.
30
-
31
- # Managing Instant Apps
32
-
33
- ## Prerequisites
34
-
35
- Look for `instant.schema.ts` and `instant.perms.ts`. These define the schema and permissions.
36
- Look for an app id and admin token in `.env` or another env file.
37
-
38
- If schema/perm files exist but the app id/admin token are missing, ask the user where to find them or whether to create a new app.
39
-
40
- To create a new app:
41
-
42
- ```bash
43
- npx instant-cli init-without-files --title <APP_NAME>
44
- ```
45
-
46
- This outputs an app id and admin token. Store them in an env file.
47
-
48
- If you get an error related to not being logged in tell the user to:
49
-
50
- - Sign up for free or log in at https://instantdb.com
51
- - Then run `npx instant-cli login` to authenticate the CLI
52
- - Then re-run the init command
53
-
54
- If you have an app id/admin token but no schema/perm files, pull them:
55
-
56
- ```bash
57
- npx instant-cli pull --yes
58
- ```
59
-
60
- ## Schema changes
61
-
62
- Edit `instant.schema.ts`, then push:
63
-
64
- ```bash
65
- npx instant-cli push schema --yes
66
- ```
67
-
68
- New fields = additions; missing fields = deletions.
69
-
70
- To rename fields:
71
-
72
- ```bash
73
- npx instant-cli push schema --rename 'posts.author:posts.creator stores.owner:stores.manager' --yes
74
- ```
75
-
76
- ## Permission changes
77
-
78
- Edit `instant.perms.ts`, then push:
79
-
80
- ```bash
81
- npx instant-cli push perms --yes
82
- ```
83
-
84
- # CRITICAL Query Guidelines
85
-
86
- CRITICAL: When using React make sure to follow the rules of hooks. Remember, you can't have hooks show up conditionally.
87
-
88
- CRITICAL: You MUST index any field you want to filter or order by in the schema. If you do not, you will get an error when you try to filter or order by it.
89
-
90
- Here is how ordering works:
91
-
92
- ```text
93
- Ordering: order: { field: 'asc' | 'desc' }
94
-
95
- Example: $: { order: { dueDate: 'asc' } }
96
-
97
- Notes: - Field must be indexed + typed in schema
98
- - Cannot order by nested attributes (e.g. 'owner.name')
99
- ```
100
-
101
- CRITICAL: Here is a concise summary of the `where` operator map which defines all the filtering options you can use with InstantDB queries to narrow results based on field values, comparisons, arrays, text patterns, and logical conditions.
102
-
103
- ```text
104
- Equality: { field: value }
105
-
106
- Inequality: { field: { $ne: value } }
107
-
108
- Null checks: { field: { $isNull: true | false } }
109
-
110
- Comparison: $gt, $lt, $gte, $lte (indexed + typed fields only)
111
-
112
- Sets: { field: { $in: [v1, v2] } }
113
-
114
- Substring: { field: { $like: 'Get%' } } // case-sensitive
115
- { field: { $ilike: '%get%' } } // case-insensitive
116
-
117
- Logic: and: [ {...}, {...} ]
118
- or: [ {...}, {...} ]
119
-
120
- Nested fields: 'relation.field': value
121
- ```
122
-
123
- CRITICAL: The operator map above is the full set of `where` filters Instant
124
- supports right now. There is no `$exists`, `$nin`, or `$regex`. And `$like` and
125
- `$ilike` are what you use for `startsWith` / `endsWith` / `includes`.
126
-
127
- CRITICAL: Pagination keys (`limit`, `offset`, `first`, `after`, `last`, `before`) only work on top-level namespaces. DO NOT use them on nested relations or else you will get an error.
128
-
129
- CRITICAL: If you are unsure how something works in InstantDB you fetch the relevant urls in the documentation to learn more.
130
-
131
- # CRITICAL Permission Guidelines
132
-
133
- Below are some CRITICAL guidelines for writing permissions in InstantDB.
134
-
135
- ## `data.ref`
136
-
137
- - Use `data.ref("<path.to.attr>")` for linked attributes.
138
- - Always returns a **list**.
139
- - Must end with an **attribute**.
140
-
141
- **Correct**
142
-
143
- ```cel
144
- auth.id in data.ref('post.author.id') // auth.id in list of author ids
145
- data.ref('owner.id') == [] // there is no owner
146
- ```
147
-
148
- **Errors**
149
-
150
- ```cel
151
- auth.id in data.post.author.id
152
- auth.id in data.ref('author')
153
- data.ref('admins.id') == auth.id
154
- auth.id == data.ref('owner.id')
155
- data.ref('owner.id') == null
156
- data.ref('owner.id').length > 0
157
- ```
158
-
159
- ## `auth.ref`
160
-
161
- - Same as `data.ref` but path must start with `$user`.
162
- - Returns a list.
163
-
164
- **Correct**
165
-
166
- ```cel
167
- 'admin' in auth.ref('$user.role.type')
168
- auth.ref('$user.role.type')[0] == 'admin'
169
- ```
170
-
171
- **Errors**
172
-
173
- ```cel
174
- auth.ref('role.type')
175
- auth.ref('$user.role.type') == 'admin'
176
- ```
177
-
178
- ## Unsupported
179
-
180
- ```cel
181
- newData.ref('x')
182
- data.ref(someVar + '.members.id')
183
- ```
184
-
185
- ## $users Permissions
186
-
187
- - Default `view` permission is `auth.id == data.id`
188
- - Default `update` and `delete` permissions is false
189
- - Default `create` permission is true (anyone can sign up)
190
- - Can override `view`, `update`, and `create`
191
- - Cannot override `delete`
192
- - The `create` rule runs during auth signup flows (not via `transact`). Use it to restrict signups or validate `extraFields`.
193
- - `extraFields` require an explicit `create` rule. Without one, signup is blocked to prevent unvalidated writes.
194
-
195
- ## $files Permissions
196
-
197
- - Default permissions are all false. Override as needed to allow access.
198
- - `data.ref` does not work for `$files` permissions.
199
- - Use `data.path.startsWith(...)` or `data.path.endsWith(...)` to write
200
- path-based rules.
201
-
202
- ## Field-level Permissions
203
-
204
- Restrict access to specific fields while keeping the entity public:
205
-
206
- ```json
207
- {
208
- "$users": {
209
- "allow": {
210
- "view": "true"
211
- },
212
- "fields": {
213
- "email": "auth.id == data.id"
214
- }
215
- }
216
- }
217
- ```
218
-
219
- Notes:
220
-
221
- - Field rules override entity-level `view` for that field
222
- - Useful for hiding sensitive data (emails, phone numbers) on public entities
223
-
224
- # CRITICAL Storage Guidelines
225
-
226
- CRITICAL: If an app displays images or files, use Instant Storage. Do not store
227
- URLs as string attributes on your entities. This includes seed scripts: do not
228
- use placeholder image URLs (e.g. picsum.photos) as string attributes to fake
229
- file support.
230
-
231
- Uploads auto-create `$files` entities. Link them to your data via the schema,
232
- then query through the relationship to get URLs.
233
-
234
- CRITICAL: You MUST include `$files` in your schema entities if you use Storage.
235
-
236
- CRITICAL: `$files` entities can only be created via `db.storage.uploadFile`. You
237
- cannot create `$files` via `db.transact`, and you cannot set `url` via transactions.
238
-
239
- ```tsx
240
- entities: {
241
- $files: i.entity({
242
- path: i.string().unique().indexed(),
243
- url: i.string(),
244
- }),
245
- posts: i.entity({
246
- caption: i.string(),
247
- }),
248
- },
249
- links: {
250
- postImage: {
251
- forward: { on: "posts", has: "one", label: "image" },
252
- reverse: { on: "$files", has: "many", label: "posts" },
253
- },
254
- }
255
-
256
- // Upload and link the returned file ID to your entity
257
- const postId = id();
258
- const { data } = await db.storage.uploadFile(`posts/${postId}/${file.name}`, file);
259
- db.transact(
260
- db.tx.posts[postId].update({ caption }).link({ image: data.id })
261
- );
262
-
263
- // Query through the relationship to get the URL
264
- const { data } = db.useQuery({ posts: { image: {} } });
265
- <img src={post.image.url} />
266
- ```
267
-
268
- # CRITICAL Rooms Guidelines
269
-
270
- CRITICAL: Hooks for presence and topics live on `db.rooms` and take the room as the first arg. The room object itself has no `usePresence` or `publishPresence` methods.
271
-
272
- Rooms host two ephemeral primitives: presence (cursor positions, who's online) and topics (live reactions). Use them only for data that should NOT persist. Persisted data via `transact` already syncs in real-time to all subscribed clients, so reach for rooms only when the data is intentionally ephemeral.
273
-
274
- ## Presence
275
-
276
- Each peer publishes a presence object readable by all other peers in the room. Retained for the connection and cleaned up automatically on disconnect.
277
-
278
- ```tsx
279
- const room = db.room('chat', 'main');
280
- const { user, peers, publishPresence } = db.rooms.usePresence(room, {
281
- initialPresence: { x: 0, y: 0 },
282
- });
283
- // peers is keyed by peerId, not an array. Use Object.values(peers) to iterate
284
- publishPresence({ x: 50, y: 50 });
285
- ```
286
-
287
- ## Topics
288
-
289
- Topic payloads aren't retained. Peers only see events fired while they're listening.
290
-
291
- ```tsx
292
- const room = db.room('chat', 'main');
293
-
294
- const publishEmoji = db.rooms.usePublishTopic(room, 'emoji');
295
- publishEmoji({ name: 'fire' });
296
-
297
- db.rooms.useTopicEffect(room, 'emoji', (payload) => {
298
- animateEmoji(payload.name);
299
- });
300
- ```
301
-
302
- # Best Practices
303
-
304
- ## Pass `schema` when initializing Instant
305
-
306
- Always pass `schema` when initializing Instant to get type safety for queries and transactions
307
-
308
- ```tsx
309
- import schema from '@/instant.schema';
310
-
311
- // On client
312
- import { init } from '@instantdb/react'; // or your relevant Instant SDK
313
- const clientDb = init({ appId, schema });
314
-
315
- // On backend
316
- import { init } from '@instantdb/admin';
317
- const adminDb = init({ appId, adminToken, schema });
318
- ```
319
-
320
- ## Use `id()` to generate ids
321
-
322
- Always use `id()` to generate ids for new entities
323
-
324
- ```tsx
325
- import { id } from '@instantdb/react'; // or your relevant Instant SDK
326
- import { clientDb } from '@/lib/clientDb';
327
- clientDb.transact(clientDb.tx.todos[id()].create({ title: 'New Todo' }));
328
- ```
329
-
330
- ## Use Instant utility types for data models
331
-
332
- Always use Instant utility types to type data models
333
-
334
- ```tsx
335
- import { AppSchema } from '@/instant.schema';
336
-
337
- type Todo = InstaQLEntity<AppSchema, 'todos'>; // todo from clientDb.useQuery({ todos: {} })
338
- type PostsWithProfile = InstaQLEntity<
339
- AppSchema,
340
- 'posts',
341
- { author: { avatar: {} } }
342
- >; // post from clientDb.useQuery({ posts: { author: { avatar: {} } } })
343
- ```
344
-
345
- ## Use `db.useAuth` or `db.subscribeAuth` for auth state
346
-
347
- ```tsx
348
- import { clientDb } from '@/lib/clientDb';
349
-
350
- // For react/react-native apps use db.useAuth
351
- function App() {
352
- const { isLoading, user, error } = clientDb.useAuth();
353
- if (isLoading) { return null; }
354
- if (error) { return <Error message={error.message} />; }
355
- if (user) { return <Main />; }
356
- return <Login />;
357
- }
358
-
359
- // For vanilla JS apps use db.subscribeAuth
360
- function App() {
361
- renderLoading();
362
- db.subscribeAuth((auth) => {
363
- if (auth.error) { renderAuthError(auth.error.message); }
364
- else if (auth.user) { renderLoggedInPage(auth.user); }
365
- else { renderSignInPage(); }
366
- });
367
- }
368
- ```
369
-
370
- ## Set custom properties at signup with `extraFields`
371
-
372
- Pass `extraFields` to any sign-in method to write custom `$users` properties atomically on user creation.
373
- Fields must be defined as optional attrs on `$users` in your schema.
374
- Use the `created` boolean to scaffold data for new users.
375
-
376
- ```tsx
377
- // Set properties at signup
378
- const { user, created } = await db.auth.signInWithMagicCode({
379
- email,
380
- code,
381
- extraFields: { nickname, createdAt: Date.now() },
382
- });
383
-
384
- // Scaffold data for new users
385
- if (created) {
386
- db.transact([
387
- db.tx.settings[id()]
388
- .update({ theme: 'light', notifications: true })
389
- .link({ user: user.id }),
390
- ]);
391
- }
392
- ```
393
-
394
- # Ad-hoc queries from the CLI
395
-
396
- Run `npx instant-cli query '{ posts: {} }' --admin` to query your app. A context flag is required: `--admin`, `--as-email <email>`, or `--as-guest`. Also supports `--app <id>`.
397
-
398
- # Instant Documentation
399
-
400
- The bullets below are links to the Instant documentation. They provide detailed information on how to use different features of InstantDB. Each line follows the pattern of
401
-
402
- - [TOPIC](URL): Description of the topic.
403
-
404
- Fetch the URL for a topic to learn more about it.
405
-
406
- - [Common mistakes](https://www.instantdb.com/docs/common-mistakes.md): Common mistakes when working with Instant
407
- - [Initializing Instant](https://www.instantdb.com/docs/init.md): How to integrate Instant with your app.
408
- - [Modeling data](https://www.instantdb.com/docs/modeling-data.md): How to model data with Instant's schema.
409
- - [Writing data](https://www.instantdb.com/docs/instaml.md): How to write data with Instant using InstaML.
410
- - [Reading data](https://www.instantdb.com/docs/instaql.md): How to read data with Instant using InstaQL.
411
- - [Instant on the Backend](https://www.instantdb.com/docs/backend.md): How to use Instant on the server with the Admin SDK.
412
- - [Patterns](https://www.instantdb.com/docs/patterns.md): Common patterns for working with InstantDB.
413
- - [Auth](https://www.instantdb.com/docs/auth/magic-codes.md): How to add magic code auth to your Instant app.
414
- - [Guest Auth](https://www.instantdb.com/docs/auth/guest-auth.md): How to add guest auth to your Instant app.
415
- - [Other Auth](https://www.instantdb.com/docs/auth.md): Additional auth methods supported by Instant.
416
- - [Managing users](https://www.instantdb.com/docs/users.md): How to manage users in your Instant app.
417
- - [Presence, Cursors, and Activity](https://www.instantdb.com/docs/presence-and-topics.md): How to add ephemeral features like presence and cursors to your Instant app.
418
- - [Instant CLI](https://www.instantdb.com/docs/cli.md): How to use the Instant CLI to manage schema.
419
- - [Storage](https://www.instantdb.com/docs/storage.md): How to upload and serve files with Instant.
420
- - [Streams](https://www.instantdb.com/docs/streams.md): How to use streams with Instant.
421
- - [Stripe Payments](https://www.instantdb.com/docs/stripe-payments.md): How to integrate Stripe payments with Instant.
422
- - [React Native](https://www.instantdb.com/docs/start-rn.md): How to use Instant in React Native apps.
423
- - [Vanilla JS](https://www.instantdb.com/docs/start-vanilla.md): How to use Instant in vanilla JS apps.
424
- - [SolidJS](https://www.instantdb.com/docs/start-solidjs.md): How to use Instant in SolidJS apps.
425
- - [Svelte](https://www.instantdb.com/docs/start-svelte.md): How to use Instant in Svelte apps.
426
- - [Vue](https://www.instantdb.com/docs/start-vue.md): How to use Instant in Vue apps.
427
- - [TanStack](https://www.instantdb.com/docs/start-tanstack.md): How to use Instant in TanStack apps.
428
-
429
- # Final Note
430
-
431
- Think before you answer. Make sure your code passes typechecks `tsc --noEmit` and works as expected.
432
- Remember! AESTHETICS ARE VERY IMPORTANT. All apps should LOOK AMAZING and have GREAT FUNCTIONALITY!