@enderworld/onlyapi 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,14 +1,421 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- var Lz=Object.create;var{getPrototypeOf:Mz,defineProperty:Qz,getOwnPropertyNames:Az}=Object;var vz=Object.prototype.hasOwnProperty;var g=(z,$,H)=>{H=z!=null?Lz(Mz(z)):{};let J=$||!z||!z.__esModule?Qz(H,"default",{value:z,enumerable:!0}):H;for(let T of Az(z))if(!vz.call(J,T))Qz(J,T,{get:()=>z[T],enumerable:!0});return J};var m=import.meta.require;var N=(z)=>`\x1B[${z}m`,w=N("0"),K=(z)=>`${N("1")}${z}${w}`,X=(z)=>`${N("2")}${z}${w}`,q=(z)=>`${N("36")}${z}${w}`,R=(z)=>`${N("32")}${z}${w}`,k=(z)=>`${N("33")}${z}${w}`,Wz=(z)=>`${N("35")}${z}${w}`,Cz=(z)=>`${N("34")}${z}${w}`,Zz=(z)=>`${N("31")}${z}${w}`,v=(z)=>`${N("90")}${z}${w}`,L=(z)=>`${N("97")}${z}${w}`;var _={success:R("\u2714"),error:Zz("\u2717"),warning:k("\u26A0"),info:q("\u2139"),arrow:q("\u2192"),chevron:q("\u203A"),sparkle:Wz("\u2726"),bolt:k("\u26A1"),folder:Cz("\uD83D\uDCC1"),file:v("\uD83D\uDCC4"),gear:v("\u2699"),rocket:Wz("\uD83D\uDE80"),package:q("\uD83D\uDCE6")},d=(z)=>{return[`${K(q(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"))}`,`${K(q(" \u2502"))} ${K(q("\u2502"))}`,`${K(q(" \u2502"))} ${K(L("\u26A1 onlyApi CLI"))} ${X(v(`v${z}`))} ${K(q("\u2502"))}`,`${K(q(" \u2502"))} ${X(v("Zero-dep enterprise REST API on Bun"))} ${K(q("\u2502"))}`,`${K(q(" \u2502"))} ${K(q("\u2502"))}`,`${K(q(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"))}`].join(`
4
- `)},Q=(z)=>process.stdout.write(`${z}
3
+ var EJ=Object.create;var{getPrototypeOf:AJ,defineProperty:WJ,getOwnPropertyNames:MJ}=Object;var RJ=Object.prototype.hasOwnProperty;var u=(J,$,K)=>{K=J!=null?EJ(AJ(J)):{};let W=$||!J||!J.__esModule?WJ(K,"default",{value:J,enumerable:!0}):K;for(let G of MJ(J))if(!RJ.call(W,G))WJ(W,G,{get:()=>J[G],enumerable:!0});return W};var p=import.meta.require;var M=(J)=>`\x1B[${J}m`,x=M("0"),H=(J)=>`${M("1")}${J}${x}`,Q=(J)=>`${M("2")}${J}${x}`,q=(J)=>`${M("36")}${J}${x}`,R=(J)=>`${M("32")}${J}${x}`,j=(J)=>`${M("33")}${J}${x}`,XJ=(J)=>`${M("35")}${J}${x}`,vJ=(J)=>`${M("34")}${J}${x}`,$J=(J)=>`${M("31")}${J}${x}`,A=(J)=>`${M("90")}${J}${x}`,E=(J)=>`${M("97")}${J}${x}`;var I={success:R("\u2714"),error:$J("\u2717"),warning:j("\u26A0"),info:q("\u2139"),arrow:q("\u2192"),chevron:q("\u203A"),sparkle:XJ("\u2726"),bolt:j("\u26A1"),folder:vJ("\uD83D\uDCC1"),file:A("\uD83D\uDCC4"),gear:A("\u2699"),rocket:XJ("\uD83D\uDE80"),package:q("\uD83D\uDCE6")},g=(J)=>{return[`${H(q(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"))}`,`${H(q(" \u2502"))} ${H(q("\u2502"))}`,`${H(q(" \u2502"))} ${H(E("\u26A1 onlyApi CLI"))} ${Q(A(`v${J}`))} ${H(q("\u2502"))}`,`${H(q(" \u2502"))} ${Q(A("Zero-dep enterprise REST API on Bun"))} ${H(q("\u2502"))}`,`${H(q(" \u2502"))} ${H(q("\u2502"))}`,`${H(q(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"))}`].join(`
4
+ `)},X=(J)=>process.stdout.write(`${J}
5
5
  `),Z=()=>process.stdout.write(`
6
- `),M=(z)=>process.stderr.write(` ${_.error} ${Zz(z)}
7
- `),h=(z)=>process.stdout.write(` ${_.warning} ${k(z)}
8
- `),C=(z)=>process.stdout.write(` ${_.info} ${z}
9
- `),e=(z)=>process.stdout.write(` ${_.success} ${R(z)}
10
- `),D=(z)=>process.stdout.write(` ${_.chevron} ${z}
11
- `),Xz=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],y=(z)=>{let $=0,H=null,J=z,T=()=>{process.stdout.write("\r\x1B[K")};return{start(){H=setInterval(()=>{T();let W=Xz[$%Xz.length]??"\u280B";process.stdout.write(` ${q(W)} ${J}`),$++},80)},update(W){J=W},stop(W){if(H)clearInterval(H);if(T(),W)process.stdout.write(` ${_.success} ${R(W)}
12
- `)}}},$z=async(z,$)=>{let H=$?` ${X(`(${$})`)}`:"";process.stdout.write(` ${_.chevron} ${z}${H}: `);let J=Bun.stdin.stream().getReader(),{value:T}=await J.read();return J.releaseLock(),(T?new TextDecoder().decode(T).trim():"")||$||""},c=async(z,$=!0)=>{let H=$?`${K("Y")}/n`:`y/${K("N")}`;process.stdout.write(` ${_.chevron} ${z} ${X(`[${H}]`)}: `);let J=Bun.stdin.stream().getReader(),{value:T}=await J.read();J.releaseLock();let W=T?new TextDecoder().decode(T).trim().toLowerCase():"";if(W==="")return $;return W==="y"||W==="yes"},zz=(z)=>{let $=Math.max(...z.map(([H])=>H.length));for(let[H,J]of z)Q(` ${v("\u2502")} ${X(H.padEnd($))} ${L(J)}`)},b=(z)=>{Z(),Q(` ${K(L(z))}`),Q(` ${v("\u2500".repeat(50))}`)};var s=(z)=>{if(z<1000)return`${Math.round(z)}ms`;if(z<60000)return`${(z/1000).toFixed(1)}s`;return`${Math.floor(z/60000)}m ${Math.round(z%60000/1000)}s`},Hz=(z=64)=>{let H=crypto.getRandomValues(new Uint8Array(z));return Array.from(H,(J)=>"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[J%62]).join("")};var Jz=(z)=>{Z(),Q(d(z)),Z(),Q(` ${K(L("USAGE"))}`),Q(` ${v("\u2500".repeat(50))}`),Q(` ${X("$")} ${q("onlyapi")} ${R("<command>")} ${X("[options]")}`),Z(),Q(` ${K(L("COMMANDS"))}`),Q(` ${v("\u2500".repeat(50))}`);let $=[["init <name>","Create a new onlyApi project"],["upgrade","Upgrade current project to latest version"],["version","Show CLI version"],["help","Show this help message"]],H=Math.max(...$.map(([B])=>B.length));for(let[B,E]of $)Q(` ${R(B.padEnd(H+2))} ${X(E)}`);Z(),Q(` ${K(L("INIT OPTIONS"))}`),Q(` ${v("\u2500".repeat(50))}`);let J=[[".","Initialize in the current directory"],["--cwd","Same as '.' \u2014 use current directory"]],T=Math.max(...J.map(([B])=>B.length));for(let[B,E]of J)Q(` ${k(B.padEnd(T+2))} ${X(E)}`);Z(),Q(` ${K(L("UPGRADE OPTIONS"))}`),Q(` ${v("\u2500".repeat(50))}`);let W=[["--force, -f","Force upgrade even if on latest version"],["--dry-run","Preview changes without applying them"]],P=Math.max(...W.map(([B])=>B.length));for(let[B,E]of W)Q(` ${k(B.padEnd(P+2))} ${X(E)}`);Z(),Q(` ${K(L("EXAMPLES"))}`),Q(` ${v("\u2500".repeat(50))}`);let I=[["onlyapi init my-api","Create project in ./my-api"],["onlyapi init .","Initialize in current directory"],["onlyapi upgrade","Upgrade to latest version"],["onlyapi upgrade --dry-run","Preview upgrade without changes"],["onlyapi upgrade --force","Force re-apply latest version"]];for(let[B,E]of I)Q(` ${X("$")} ${q(B)}`),Q(` ${X(E)}`);Z(),Q(` ${X("Docs:")} ${q("https://github.com/lysari/onlyapi#readme")}`),Q(` ${X("Issues:")} ${q("https://github.com/lysari/onlyapi/issues")}`),Z()};import{existsSync as l,mkdirSync as Nz,rmSync as Kz}from"fs";import{join as o,resolve as Rz}from"path";var qz="https://github.com/lysari/onlyapi.git",Vz="https://github.com/lysari/onlyapi/archive/refs/heads/main.tar.gz",wz=/^[a-zA-Z0-9_-]+$/,Dz=(z)=>{if(!z)return"Project name is required.";if(!wz.test(z))return"Project name can only contain letters, numbers, hyphens, and underscores.";if(z.length>214)return"Project name is too long (max 214 chars).";return null},u=async(z,$=process.cwd())=>{let H=Bun.spawn(z,{cwd:$,stdout:"pipe",stderr:"pipe"}),[J,T]=await Promise.all([new Response(H.stdout).text(),new Response(H.stderr).text()]),W=await H.exited;return{stdout:J.trim(),stderr:T.trim(),exitCode:W}},xz=async(z)=>{try{let{exitCode:$}=await u(["which",z]);return $===0}catch{return!1}},Yz=async(z,$)=>{let H=performance.now();Z(),Q(d($)),Z();let J=z[0]??"",T=z.includes("--cwd")||z.includes(".");if(!J&&!T)J=await $z("Project name","my-api");if(T)J=".";if(J!=="."){let Y=Dz(J);if(Y)M(Y),process.exit(1)}let W=J==="."?process.cwd():Rz(process.cwd(),J);if(J!=="."&&l(W)){if((await Array.fromAsync(new Bun.Glob("*").scan({cwd:W}))).length>0){if(!await c(`Directory ${K(L(J))} already exists and is not empty. Continue?`,!1))C("Aborted."),process.exit(0)}}if(b("Creating project"),J!==".")Nz(W,{recursive:!0}),D(`Created directory ${K(q(J))}`);let P=await xz("git"),I=y("Downloading template...");I.start();let B=!1;if(P){I.update("Cloning from GitHub...");let{exitCode:Y}=await u(["git","clone","--depth=1","--single-branch",qz,J==="."?".":J],J==="."?W:process.cwd());B=Y===0}if(!B){I.update("Downloading release archive...");try{let Y=await fetch(Vz);if(!Y.ok)throw Error(`HTTP ${Y.status}`);let G=o(W,"__onlyapi.tar.gz");await Bun.write(G,Y),I.update("Extracting..."),await u(["tar","xzf",G,"--strip-components=1"],W),Kz(G,{force:!0}),B=!0}catch(Y){I.stop(),M(`Failed to download template: ${Y instanceof Error?Y.message:String(Y)}`),M("Please check your network connection and try again."),Z(),C(`You can also clone manually: ${X(`git clone ${qz} ${J}`)}`),process.exit(1)}}I.stop("Template downloaded");let E=o(W,".git");if(l(E))Kz(E,{recursive:!0,force:!0});if(P)await u(["git","init"],W),D("Initialized fresh git repository");let f=o(W,"package.json");if(l(f))try{let Y=await Bun.file(f).text(),G=JSON.parse(Y);if(J!==".")G.name=J;G.version="0.1.0",G.description="",G.author="",G.repository=void 0,G.bugs=void 0,G.homepage=void 0,await Bun.write(f,`${JSON.stringify(G,null,2)}
13
- `),D(`Updated ${K(q("package.json"))}`)}catch{h("Could not update package.json \u2014 you can edit it manually")}let O=o(W,".env.example"),S=o(W,".env");if(l(O)&&!l(S))try{let Y=await Bun.file(O).text(),G=Hz(64);Y=Y.replace("change-me-to-a-64-char-random-string",G),await Bun.write(S,Y),D(`Generated ${K(q(".env"))} with secure JWT_SECRET`)}catch{h("Could not generate .env \u2014 copy .env.example manually")}b("Installing dependencies");let p=y("Running bun install...");p.start();let{exitCode:j,stderr:t}=await u(["bun","install"],W);if(j!==0)p.stop(),M("Failed to install dependencies:"),Q(` ${X(t)}`),Z(),C(`Run ${K(q("bun install"))} manually in the project directory.`);else p.stop("Dependencies installed");if(P)await u(["git","add","-A"],W),await u(["git","commit","-m","Initial commit from onlyApi CLI","--no-verify"],W),D("Created initial commit");let r=performance.now()-H;Z(),Q(` ${_.rocket} ${K(R("Project created successfully!"))} ${X(`(${s(r)})`)}`),Z(),b("Next steps"),Z();let F=J!=="."?`cd ${J}`:null,U=[...F?[F]:[],"bun run dev # Start dev server (hot-reload)","bun test # Run tests","bun run check # Type-check"];for(let Y of U)Q(` ${X("$")} ${K(q(Y))}`);Z(),Q(` ${X("Docs:")} ${q("https://github.com/lysari/onlyapi#readme")}`),Q(` ${X("Issues:")} ${q("https://github.com/lysari/onlyapi/issues")}`),Z(),Q(` ${X("Happy hacking!")} ${_.bolt}`),Z()};import{existsSync as V}from"fs";import{join as x,resolve as Pz}from"path";var Bz="https://api.github.com/repos/lysari/onlyapi",fz=(z)=>`https://github.com/lysari/onlyapi/archive/refs/tags/${z}.tar.gz`,Fz="https://github.com/lysari/onlyapi/archive/refs/heads/main.tar.gz",Sz="https://registry.npmjs.org/only-api",jz=["src/core/errors/app-error.ts","src/core/types/brand.ts","src/core/types/result.ts","src/infrastructure/logging/logger.ts","src/infrastructure/security/password-hasher.ts","src/infrastructure/security/token-service.ts","src/presentation/middleware/cors.ts","src/presentation/middleware/rate-limit.ts","src/presentation/middleware/security-headers.ts","src/presentation/server.ts","src/presentation/context.ts","src/shared/cli.ts","src/shared/container.ts","src/shared/utils/id.ts","src/shared/utils/timing-safe.ts","src/shared/log-format.ts","src/cluster.ts","tsconfig.json","biome.json"],i=async(z,$=process.cwd())=>{let H=Bun.spawn(z,{cwd:$,stdout:"pipe",stderr:"pipe"}),[J,T]=await Promise.all([new Response(H.stdout).text(),new Response(H.stderr).text()]),W=await H.exited;return{stdout:J.trim(),stderr:T.trim(),exitCode:W}},Gz=(z)=>{let $=z.replace(/^v/,"").split(".").map(Number);return[$[0]??0,$[1]??0,$[2]??0]},Tz=(z,$)=>{let[H,J,T]=Gz(z),[W,P,I]=Gz($);if(H!==W)return H>W;if(J!==P)return J>P;return T>I},hz=async()=>{try{let z=await fetch(`${Bz}/releases/latest`,{headers:{Accept:"application/vnd.github.v3+json"}});if(z.ok)return(await z.json()).tag_name.replace(/^v/,"")}catch{}try{let z=await fetch(`${Bz}/tags?per_page=1`,{headers:{Accept:"application/vnd.github.v3+json"}});if(z.ok){let $=await z.json();if($.length>0){let H=$[0];return H?H.name.replace(/^v/,""):null}}}catch{}try{let z=await fetch(Sz);if(z.ok)return(await z.json())["dist-tags"].latest}catch{}return null},Uz=async(z,$)=>{let H=performance.now(),J=Pz(process.cwd());Z(),Q(d($)),Z();let T=x(J,"package.json");if(!V(T))M("No package.json found in current directory."),C("Run this command from the root of your onlyApi project."),process.exit(1);let W;try{W=JSON.parse(await Bun.file(T).text()).version??"0.0.0"}catch{M("Could not read package.json."),process.exit(1)}if(!(V(x(J,"src/main.ts"))&&V(x(J,"src/core"))&&V(x(J,"src/presentation"))))M("This doesn't appear to be an onlyApi project."),C("Expected to find src/main.ts, src/core/, and src/presentation/"),process.exit(1);b("Checking for updates");let I=y("Fetching latest version...");I.start();let B=await hz();if(!B){if(I.stop(),h("Could not determine the latest version."),C("This may be due to network issues or API rate limits."),Z(),!(z.includes("--force")||z.includes("-f"))){if(!await c("Continue with upgrade from main branch?",!1))C("Aborted."),process.exit(0)}}else{if(I.stop("Version check complete"),Z(),zz([["Current version",W],["Latest version",B]]),Z(),!Tz(B,W)&&!z.includes("--force")&&!z.includes("-f"))e("You're already on the latest version!"),Z(),process.exit(0);if(Tz(B,W))C(`Update available: ${K(k(W))} ${X("\u2192")} ${K(R(B))}`);else C(`Re-applying latest version ${X("(--force)")}`)}let E=V(x(J,".git"));if(E){let{stdout:F}=await i(["git","status","--porcelain"],J);if(F){if(Z(),h("You have uncommitted changes."),!await c("Continue anyway?",!1))C("Commit your changes first, then retry."),process.exit(0)}}b("Downloading update");let f=y("Downloading latest source...");f.start();let O=x(J,".onlyapi-upgrade-tmp");try{if(V(O)){let{rmSync:Ez}=await import("fs");Ez(O,{recursive:!0,force:!0})}let{mkdirSync:F}=await import("fs");F(O,{recursive:!0});let U=B?fz(`v${B}`):Fz,Y=await fetch(U),G=Y;if(!Y.ok&&B)f.update("Tag not found, trying main branch..."),G=await fetch(Fz);if(!G.ok)throw Error(`HTTP ${G.status}`);let A=x(O,"update.tar.gz");await Bun.write(A,G),f.update("Extracting..."),await i(["tar","xzf",A,"--strip-components=1"],O);let{rmSync:n}=await import("fs");n(A,{force:!0}),f.stop("Download complete")}catch(F){if(f.stop(),M(`Failed to download update: ${F instanceof Error?F.message:String(F)}`),V(O)){let{rmSync:U}=await import("fs");U(O,{recursive:!0,force:!0})}process.exit(1)}b("Applying updates");let S=0,p=0,j=z.includes("--dry-run");for(let F of jz){let U=x(O,F),Y=x(J,F);if(!V(U))continue;try{let G=await Bun.file(U).text();if(V(Y)){if(await Bun.file(Y).text()===G){p++;continue}}if(!j){let A=Y.substring(0,Y.lastIndexOf("/")),{mkdirSync:n}=await import("fs");n(A,{recursive:!0}),await Bun.write(Y,G)}D(`${j?`${X("[dry-run]")} `:""}Updated ${K(q(F))}`),S++}catch{h(`Could not update ${F}`)}}let t=x(O,"package.json");if(V(t))try{let F=JSON.parse(await Bun.file(t).text()),U=JSON.parse(await Bun.file(T).text()),Y=!1;if(F.dependencies){U.dependencies=U.dependencies??{};for(let[G,A]of Object.entries(F.dependencies))if(U.dependencies[G]!==A)U.dependencies[G]=A,Y=!0}if(F.devDependencies){U.devDependencies=U.devDependencies??{};for(let[G,A]of Object.entries(F.devDependencies))if(U.devDependencies[G]!==A)U.devDependencies[G]=A,Y=!0}if(B)U.version=B;if(!j)await Bun.write(T,`${JSON.stringify(U,null,2)}
14
- `);if(Y)D(`${j?`${X("[dry-run]")} `:""}Updated dependencies in ${K(q("package.json"))}`)}catch{h("Could not merge package.json dependencies")}if(V(O)){let{rmSync:F}=await import("fs");F(O,{recursive:!0,force:!0})}if(!j&&S>0){b("Installing dependencies");let F=y("Running bun install...");F.start();let{exitCode:U}=await i(["bun","install"],J);if(U!==0)F.stop(),h("bun install failed \u2014 run it manually.");else F.stop("Dependencies installed")}if(E&&!j&&S>0){if(await c("Create a git commit for this upgrade?")){let U=B?`chore: upgrade onlyApi to v${B}`:"chore: upgrade onlyApi to latest";await i(["git","add","-A"],J),await i(["git","commit","-m",U,"--no-verify"],J),D("Created upgrade commit")}}let r=performance.now()-H;if(Z(),S>0)Q(` ${_.rocket} ${K(R("Upgrade complete!"))} ${X(`(${s(r)})`)}`),Z(),zz([["Files updated",String(S)],["Files unchanged",String(p)]]);else if(j)Q(` ${_.info} ${K(q("Dry run complete"))} \u2014 no files were modified.`);else e("All files are already up to date!");if(Z(),S>0)Q(` ${X("Note: The following files are NOT auto-upgraded (your custom code):")}`),Q(` ${X(" - src/application/ (your services & DTOs)")}`),Q(` ${X(" - src/core/entities/ (your domain entities)")}`),Q(` ${X(" - src/core/ports/ (your port interfaces)")}`),Q(` ${X(" - src/presentation/handlers/ (your route handlers)")}`),Q(` ${X(" - src/presentation/routes/ (your routes)")}`),Q(` ${X(" - src/main.ts (your bootstrap)")}`),Z(),Q(` ${X("Review the")} ${q("CHANGELOG.md")} ${X("for breaking changes.")}`),Z()};var a="1.5.1",_z=process.argv.slice(2),Iz=_z[0]?.toLowerCase()??"",Oz=_z.slice(1),bz=async()=>{try{switch(Iz){case"init":case"create":case"new":await Yz(Oz,a);break;case"upgrade":case"update":await Uz(Oz,a);break;case"version":case"-v":case"--version":Q(`onlyapi v${a}`);break;case"help":case"-h":case"--help":Jz(a);break;case"":Jz(a);break;default:Z(),M(`Unknown command: ${K(L(Iz))}`),Z(),Q(` ${X("Run")} ${q("onlyapi help")} ${X("to see available commands.")}`),Z(),process.exit(1)}}catch(z){Z(),M(z instanceof Error?z.message:String(z)),Z(),process.exit(1)}};bz();
6
+ `),v=(J)=>process.stderr.write(` ${I.error} ${$J(J)}
7
+ `),k=(J)=>process.stdout.write(` ${I.warning} ${j(J)}
8
+ `),C=(J)=>process.stdout.write(` ${I.info} ${J}
9
+ `),n=(J)=>process.stdout.write(` ${I.success} ${R(J)}
10
+ `),b=(J)=>process.stdout.write(` ${I.chevron} ${J}
11
+ `),ZJ=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],y=(J)=>{let $=0,K=null,W=J,G=()=>{process.stdout.write("\r\x1B[K")};return{start(){K=setInterval(()=>{G();let z=ZJ[$%ZJ.length]??"\u280B";process.stdout.write(` ${q(z)} ${W}`),$++},80)},update(z){W=z},stop(z){if(K)clearInterval(K);if(G(),z)process.stdout.write(` ${I.success} ${R(z)}
12
+ `)}}},zJ=async(J,$)=>{let K=$?` ${Q(`(${$})`)}`:"";process.stdout.write(` ${I.chevron} ${J}${K}: `);let W=Bun.stdin.stream().getReader(),{value:G}=await W.read();return W.releaseLock(),(G?new TextDecoder().decode(G).trim():"")||$||""},m=async(J,$=!0)=>{let K=$?`${H("Y")}/n`:`y/${H("N")}`;process.stdout.write(` ${I.chevron} ${J} ${Q(`[${K}]`)}: `);let W=Bun.stdin.stream().getReader(),{value:G}=await W.read();W.releaseLock();let z=G?new TextDecoder().decode(G).trim().toLowerCase():"";if(z==="")return $;return z==="y"||z==="yes"},e=(J)=>{let $=Math.max(...J.map(([K])=>K.length));for(let[K,W]of J)X(` ${A("\u2502")} ${Q(K.padEnd($))} ${E(W)}`)},V=(J)=>{Z(),X(` ${H(E(J))}`),X(` ${A("\u2500".repeat(50))}`)};var i=(J)=>{if(J<1000)return`${Math.round(J)}ms`;if(J<60000)return`${(J/1000).toFixed(1)}s`;return`${Math.floor(J/60000)}m ${Math.round(J%60000/1000)}s`},HJ=(J=64)=>{let K=crypto.getRandomValues(new Uint8Array(J));return Array.from(K,(W)=>"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[W%62]).join("")};var JJ=(J)=>{Z(),X(g(J)),Z(),X(` ${H(E("USAGE"))}`),X(` ${A("\u2500".repeat(50))}`),X(` ${Q("$")} ${q("onlyapi")} ${R("<command>")} ${Q("[options]")}`),Z(),X(` ${H(E("COMMANDS"))}`),X(` ${A("\u2500".repeat(50))}`);let $=[["init <name>","Create a new onlyApi project"],["upgrade","Upgrade current project to latest version"],["version","Show CLI version"],["help","Show this help message"]],K=Math.max(...$.map(([Y])=>Y.length));for(let[Y,L]of $)X(` ${R(Y.padEnd(K+2))} ${Q(L)}`);Z(),X(` ${H(E("INIT OPTIONS"))}`),X(` ${A("\u2500".repeat(50))}`);let W=[[".","Initialize in the current directory"],["--cwd","Same as '.' \u2014 use current directory"]],G=Math.max(...W.map(([Y])=>Y.length));for(let[Y,L]of W)X(` ${j(Y.padEnd(G+2))} ${Q(L)}`);Z(),X(` ${H(E("UPGRADE OPTIONS"))}`),X(` ${A("\u2500".repeat(50))}`);let z=[["--force, -f","Force upgrade even if on latest version"],["--dry-run","Preview changes without applying them"]],w=Math.max(...z.map(([Y])=>Y.length));for(let[Y,L]of z)X(` ${j(Y.padEnd(w+2))} ${Q(L)}`);Z(),X(` ${H(E("EXAMPLES"))}`),X(` ${A("\u2500".repeat(50))}`);let D=[["onlyapi init my-api","Create project in ./my-api"],["onlyapi init .","Initialize in current directory"],["onlyapi upgrade","Upgrade to latest version"],["onlyapi upgrade --dry-run","Preview upgrade without changes"],["onlyapi upgrade --force","Force re-apply latest version"]];for(let[Y,L]of D)X(` ${Q("$")} ${q(Y)}`),X(` ${Q(L)}`);Z(),X(` ${Q("Docs:")} ${q("https://github.com/lysari/onlyapi#readme")}`),X(` ${Q("Issues:")} ${q("https://github.com/lysari/onlyapi/issues")}`),Z()};import{existsSync as t,mkdirSync as qJ,writeFileSync as CJ}from"fs";import{dirname as VJ,join as QJ,resolve as NJ}from"path";var KJ=(J)=>[{path:"package.json",content:`{
13
+ "name": ${JSON.stringify(J)},
14
+ "version": "0.1.0",
15
+ "type": "module",
16
+ "scripts": {
17
+ "dev": "bun --watch src/main.ts",
18
+ "start": "NODE_ENV=production bun src/main.ts",
19
+ "check": "tsc --noEmit",
20
+ "test": "bun test",
21
+ "lint": "bunx @biomejs/biome check src/"
22
+ },
23
+ "dependencies": {
24
+ "zod": "^3.24.2"
25
+ },
26
+ "devDependencies": {
27
+ "@biomejs/biome": "^1.9.4",
28
+ "@types/bun": "^1.2.2",
29
+ "typescript": "^5.7.3"
30
+ }
31
+ }
32
+ `},{path:"tsconfig.json",content:`{
33
+ "compilerOptions": {
34
+ "target": "ESNext",
35
+ "module": "ESNext",
36
+ "moduleResolution": "bundler",
37
+ "lib": ["ESNext"],
38
+ "types": ["bun-types"],
39
+ "strict": true,
40
+ "noImplicitAny": true,
41
+ "noUnusedLocals": true,
42
+ "noUnusedParameters": true,
43
+ "esModuleInterop": true,
44
+ "skipLibCheck": true,
45
+ "outDir": "dist",
46
+ "rootDir": "."
47
+ },
48
+ "include": ["src/**/*.ts", "tests/**/*.ts"],
49
+ "exclude": ["node_modules", "dist"]
50
+ }
51
+ `},{path:"biome.json",content:`{
52
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
53
+ "organizeImports": { "enabled": true },
54
+ "linter": {
55
+ "enabled": true,
56
+ "rules": { "recommended": true }
57
+ },
58
+ "formatter": {
59
+ "enabled": true,
60
+ "indentStyle": "space",
61
+ "indentWidth": 2,
62
+ "lineWidth": 100
63
+ }
64
+ }
65
+ `},{path:".env.example",content:`# Environment
66
+ NODE_ENV=development
67
+ PORT=3000
68
+ HOST=0.0.0.0
69
+
70
+ # Security
71
+ JWT_SECRET=change-me-to-a-64-char-random-string
72
+ JWT_EXPIRES_IN=15m
73
+
74
+ # Logging
75
+ LOG_LEVEL=debug
76
+
77
+ # Database
78
+ DATABASE_PATH=data/app.sqlite
79
+ `},{path:".gitignore",content:`node_modules/
80
+ dist/
81
+ data/
82
+ .env
83
+ *.sqlite
84
+ *.log
85
+ `},{path:".dockerignore",content:`node_modules/
86
+ dist/
87
+ data/
88
+ .env
89
+ .git/
90
+ *.md
91
+ `},{path:"Dockerfile",content:`FROM oven/bun:1.3-alpine AS builder
92
+ WORKDIR /app
93
+ COPY package.json bun.lock* ./
94
+ RUN bun install --frozen-lockfile
95
+ COPY tsconfig.json ./
96
+ COPY src/ src/
97
+ RUN bun run check
98
+ RUN mkdir -p /app/data
99
+
100
+ FROM oven/bun:1.3-alpine
101
+ WORKDIR /app
102
+ COPY --from=builder /app/src/ src/
103
+ COPY --from=builder /app/node_modules/ node_modules/
104
+ COPY --from=builder /app/package.json ./
105
+ COPY --from=builder /app/tsconfig.json ./
106
+ COPY --from=builder /app/data/ data/
107
+ RUN addgroup -S app && adduser -S app -G app && chown -R app:app /app
108
+ USER app
109
+ EXPOSE 3000
110
+ ENV NODE_ENV=production
111
+ ENV HOST=0.0.0.0
112
+ ENV PORT=3000
113
+ CMD ["bun", "src/main.ts"]
114
+ `},{path:"README.md",content:`# ${J}
115
+
116
+ Built with [onlyApi](https://github.com/lysari/onlyapi) \u2014 zero-dependency REST API on Bun.
117
+
118
+ ## Quick Start
119
+
120
+ \`\`\`bash
121
+ bun run dev # Start dev server (hot-reload)
122
+ bun test # Run tests
123
+ bun run check # Type-check
124
+ \`\`\`
125
+
126
+ ## API Endpoints
127
+
128
+ | Method | Path | Description | Auth |
129
+ |--------|-------------------------|-------------------|------|
130
+ | GET | /health | Health check | No |
131
+ | POST | /api/v1/auth/register | Register | No |
132
+ | POST | /api/v1/auth/login | Login | No |
133
+ | POST | /api/v1/auth/logout | Logout | Yes |
134
+ | GET | /api/v1/users/me | Get profile | Yes |
135
+ | PATCH | /api/v1/users/me | Update profile | Yes |
136
+ | DELETE | /api/v1/users/me | Delete account | Yes |
137
+
138
+ ## Project Structure
139
+
140
+ \`\`\`
141
+ src/
142
+ main.ts # Entry point \u2014 wires everything together
143
+ config.ts # Environment config with validation
144
+ database.ts # SQLite setup + migrations
145
+ logger.ts # Colored structured logger
146
+ router.ts # Lightweight router with route table
147
+ server.ts # HTTP server, CORS, rate limiting
148
+ handlers/
149
+ auth.handler.ts # Register, login, logout
150
+ health.handler.ts # Health check
151
+ user.handler.ts # User profile CRUD
152
+ middleware/
153
+ auth.ts # JWT authentication guard
154
+ services/
155
+ auth.service.ts # Auth business logic
156
+ user.service.ts # User business logic
157
+ utils/
158
+ password.ts # Argon2id hashing
159
+ token.ts # JWT sign/verify
160
+ response.ts # JSON response helpers
161
+ \`\`\`
162
+
163
+ ## Environment Variables
164
+
165
+ | Variable | Default | Description |
166
+ |----------------|----------------------|-------------------------|
167
+ | PORT | 3000 | Server port |
168
+ | HOST | 0.0.0.0 | Bind address |
169
+ | JWT_SECRET | \u2014 | **Required** JWT secret |
170
+ | JWT_EXPIRES_IN | 15m | Token expiration |
171
+ | DATABASE_PATH | data/app.sqlite | SQLite file path |
172
+ | LOG_LEVEL | debug | info / debug / warn |
173
+ | NODE_ENV | development | development / production|
174
+
175
+ ## Docker
176
+
177
+ \`\`\`bash
178
+ docker build -t ${J} .
179
+ docker run -e JWT_SECRET="your-secret-here" -p 3000:3000 ${J}
180
+ \`\`\`
181
+ `},{path:"src/logger.ts",content:["/**"," * Colored structured logger \u2014 zero dependencies."," */","",'type LogLevel = "debug" | "info" | "warn" | "error";',"","const LEVELS: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };","","// ANSI colors",'const esc = (c: string) => "\\x1b[" + c + "m";','const reset = esc("0");','const bold = (s: string) => esc("1") + s + reset;','const dim = (s: string) => esc("2") + s + reset;','const green = (s: string) => esc("32") + s + reset;','const yellow = (s: string) => esc("33") + s + reset;','const red = (s: string) => esc("31") + s + reset;','const cyan = (s: string) => esc("36") + s + reset;','const gray = (s: string) => esc("90") + s + reset;','const white = (s: string) => esc("97") + s + reset;','const magenta = (s: string) => esc("35") + s + reset;','const blue = (s: string) => esc("34") + s + reset;',"","const timestamp = (): string => {"," const d = new Date();",' const h = String(d.getHours()).padStart(2, "0");',' const m = String(d.getMinutes()).padStart(2, "0");',' const s = String(d.getSeconds()).padStart(2, "0");',' const ms = String(d.getMilliseconds()).padStart(3, "0");',' return h + ":" + m + ":" + s + "." + ms;',"};","","const badge = (level: LogLevel): string => {"," switch (level) {",' case "debug": return gray("DBG");',' case "info": return green("INF");',' case "warn": return yellow("WRN");',' case "error": return red("ERR");'," }","};","","export interface Logger {"," debug(msg: string, extra?: Record<string, unknown>): void;"," info(msg: string, extra?: Record<string, unknown>): void;"," warn(msg: string, extra?: Record<string, unknown>): void;"," error(msg: string, extra?: Record<string, unknown>): void;","}","",'export const createLogger = (minLevel: LogLevel = "debug"): Logger => {'," const threshold = LEVELS[minLevel];",""," const log = (level: LogLevel, msg: string, extra?: Record<string, unknown>) => {"," if (LEVELS[level] < threshold) return;",' let line = " " + badge(level) + " " + dim(timestamp()) + " " + msg;'," if (extra) {"," const parts = Object.entries(extra)",' .map(([k, v]) => gray(k + "=") + white(String(v)));',' line += " " + parts.join(" ");'," }"," console.log(line);"," };",""," return {",' debug: (msg, extra) => log("debug", msg, extra),',' info: (msg, extra) => log("info", msg, extra),',' warn: (msg, extra) => log("warn", msg, extra),',' error: (msg, extra) => log("error", msg, extra),'," };","};","","// \u2500\u2500 Request logging \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","","const methodColor = (method: string): string => {"," switch (method) {",' case "GET": return green(bold(method.padEnd(7)));',' case "POST": return cyan(bold(method.padEnd(7)));',' case "PATCH": return yellow(bold(method.padEnd(7)));',' case "PUT": return yellow(bold(method.padEnd(7)));',' case "DELETE": return red(bold(method.padEnd(7)));'," default: return white(bold(method.padEnd(7)));"," }","};","","const statusColor = (status: number): string => {"," if (status < 300) return green(String(status));"," if (status < 400) return cyan(String(status));"," if (status < 500) return yellow(String(status));"," return red(String(status));","};","","export const formatRequest = ("," method: string,"," path: string,"," status: number,"," durationMs: number,"," ip: string,","): string => {",' const arrow = gray("\u2190");',' const dur = dim(durationMs.toFixed(2) + "ms");'," return (",' " " + arrow + " " +',' dim(timestamp()) + " " +',' methodColor(method) + " " +',' statusColor(status) + " " +',' path + " " +',' dur + " " +',' gray("ip=" + ip)'," );","};","","// \u2500\u2500 Startup banner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","","export { bold, cyan, dim, gray, green, magenta, blue, white, yellow, red };",""].join(`
182
+ `)},{path:"src/router.ts",content:["/**"," * Lightweight router with O(1) static route lookup."," */","","type Handler = (req: Request, params?: Record<string, string>) => Promise<Response> | Response;","","interface Route {"," method: string;"," path: string;"," handler: Handler;"," auth: boolean;"," description: string;","}","","export interface Router {"," add(method: string, path: string, handler: Handler, opts?: { auth?: boolean; description?: string }): void;"," match(method: string, path: string): { handler: Handler; auth: boolean } | null;"," routes(): ReadonlyArray<{ method: string; path: string; auth: boolean; description: string }>;","}","","export const createRouter = (): Router => {"," const table = new Map<string, Route>();"," const list: Route[] = [];",""," return {"," add(method, path, handler, opts = {}) {",' const key = method + " " + path;',' const route: Route = { method, path, handler, auth: opts.auth ?? false, description: opts.description ?? "" };'," table.set(key, route);"," list.push(route);"," },",""," match(method, path) {",' const route = table.get(method + " " + path);'," if (route) return { handler: route.handler, auth: route.auth };"," return null;"," },",""," routes() {"," return list.map(r => ({ method: r.method, path: r.path, auth: r.auth, description: r.description }));"," },"," };","};",""].join(`
183
+ `)},{path:"src/main.ts",content:['import { loadConfig } from "./config.js";','import { createDatabase } from "./database.js";','import { createLogger, bold, cyan, dim, gray, green, white, magenta, blue, yellow } from "./logger.js";','import { createRouter } from "./router.js";','import { createAuthService } from "./services/auth.service.js";','import { createUserService } from "./services/user.service.js";','import { createServer } from "./server.js";','import { createPasswordHasher } from "./utils/password.js";','import { createTokenService } from "./utils/token.js";',"","const startTime = performance.now();","","// \u2500\u2500 Config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","const config = loadConfig();","const log = createLogger(config.logLevel);","","// \u2500\u2500 Database \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","const db = createDatabase(config.databasePath, log);","","// \u2500\u2500 Services \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","const passwordHasher = createPasswordHasher();","const tokenService = createTokenService(config.jwt.secret, config.jwt.expiresIn);","const authService = createAuthService(db, passwordHasher, tokenService);","const userService = createUserService(db, passwordHasher);","","// \u2500\u2500 Router \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","const router = createRouter();","",'import { authHandlers } from "./handlers/auth.handler.js";','import { healthHandler } from "./handlers/health.handler.js";','import { userHandlers } from "./handlers/user.handler.js";',"","const auth = authHandlers(authService);","const health = healthHandler();","const users = userHandlers(userService);","","// Public",'router.add("GET", "/health", health.check, { description: "Health check" });','router.add("POST", "/api/v1/auth/register", auth.register, { description: "Register user" });','router.add("POST", "/api/v1/auth/login", auth.login, { description: "Login" });',"","// Protected",'router.add("POST", "/api/v1/auth/logout", auth.logout, { auth: true, description: "Logout" });','router.add("GET", "/api/v1/users/me", users.getProfile, { auth: true, description: "Get profile" });','router.add("PATCH", "/api/v1/users/me", users.updateProfile, { auth: true, description: "Update profile" });','router.add("DELETE", "/api/v1/users/me", users.deleteAccount, { auth: true, description: "Delete account" });',"","// \u2500\u2500 Server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","const server = createServer({ config, router, tokenService, log });","","// \u2500\u2500 Startup banner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","const bootMs = performance.now() - startTime;","",'const envBadge = config.nodeEnv === "production"',' ? "\\x1b[42m\\x1b[30m PRODUCTION \\x1b[0m"',' : "\\x1b[46m\\x1b[30m DEVELOPMENT \\x1b[0m";',"",'console.log("");','console.log(bold(cyan(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510")));','console.log(bold(cyan(" \u2502")) + " " + bold(cyan("\u2502")));',`console.log(bold(cyan(" \u2502")) + " " + bold(white("\u26A1 " + ${JSON.stringify(J)})) + " " + bold(cyan("\u2502")));`,'console.log(bold(cyan(" \u2502")) + " " + dim(gray("Built with onlyApi")) + " " + bold(cyan("\u2502")));','console.log(bold(cyan(" \u2502")) + " " + bold(cyan("\u2502")));','console.log(bold(cyan(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518")));','console.log("");','console.log(" " + envBadge + " " + dim("booted in") + " " + bold(green(bootMs.toFixed(0) + "ms")));','console.log("");','console.log(" " + bold(white("\u2192")) + " " + dim("Local:") + " " + bold(cyan("http://localhost:" + config.port)));','console.log(" " + bold(white("\u2192")) + " " + dim("Network:") + " " + bold(cyan("http://" + config.host + ":" + config.port)));','console.log(" " + bold(white("\u2192")) + " " + dim("SQLite:") + " " + dim(config.databasePath));','console.log("");',"","// Process info",'console.log(" " + gray("\u251C\u2500") + " " + dim("PID") + " " + white(String(process.pid)));','console.log(" " + gray("\u251C\u2500") + " " + dim("Runtime") + " " + magenta("Bun " + Bun.version));','console.log(" " + gray("\u251C\u2500") + " " + dim("TypeScript") + " " + blue("strict"));','console.log(" " + gray("\u251C\u2500") + " " + dim("Rate limit") + " " + white(config.rateLimitMax + " req/" + (config.rateLimitWindowMs / 1000) + "s"));','console.log(" " + gray("\u2514\u2500") + " " + dim("Log level") + " " + white(config.logLevel));','console.log("");',"","// Route table","const allRoutes = router.routes();",'console.log(" " + bold(white("Routes")) + " " + dim("(" + allRoutes.length + ")"));','console.log(" " + gray("\u2500".repeat(60)));',"for (const r of allRoutes) {",' const lock = r.auth ? yellow("\uD83D\uDD12") : " ";'," const mc = (() => {"," switch (r.method) {",' case "GET": return green(bold(r.method.padEnd(7)));',' case "POST": return cyan(bold(r.method.padEnd(7)));',' case "PATCH": return yellow(bold(r.method.padEnd(7)));',' case "DELETE": return "\\x1b[31m\\x1b[1m" + r.method.padEnd(7) + "\\x1b[0m";'," default: return white(bold(r.method.padEnd(7)));"," }"," })();",' console.log(" " + lock + " " + mc + " " + r.path.padEnd(30) + " " + dim(gray(r.description)));',"}",'console.log(" " + gray("\u2500".repeat(60)));','console.log("");','console.log(" " + dim("press Ctrl+C to stop"));','console.log("");',"","// \u2500\u2500 Graceful shutdown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","const shutdown = () => {",' log.info("Shutting down...");'," server.stop();"," db.close();"," process.exit(0);","};","",'process.on("SIGINT", shutdown);','process.on("SIGTERM", shutdown);',""].join(`
184
+ `)},{path:"src/config.ts",content:`import { z } from "zod";
185
+
186
+ const configSchema = z.object({
187
+ port: z.coerce.number().default(3000),
188
+ host: z.string().default("0.0.0.0"),
189
+ nodeEnv: z.enum(["development", "production"]).default("development"),
190
+ jwt: z.object({
191
+ secret: z.string().min(32, "JWT_SECRET must be at least 32 characters"),
192
+ expiresIn: z.string().default("15m"),
193
+ }),
194
+ databasePath: z.string().default("data/app.sqlite"),
195
+ logLevel: z.enum(["debug", "info", "warn", "error"]).default("debug"),
196
+ corsOrigins: z.string().default("*"),
197
+ rateLimitMax: z.coerce.number().default(100),
198
+ rateLimitWindowMs: z.coerce.number().default(60_000),
199
+ });
200
+
201
+ export type AppConfig = z.infer<typeof configSchema>;
202
+
203
+ export const loadConfig = (): AppConfig => {
204
+ const result = configSchema.safeParse({
205
+ port: Bun.env.PORT,
206
+ host: Bun.env.HOST,
207
+ nodeEnv: Bun.env.NODE_ENV,
208
+ jwt: {
209
+ secret: Bun.env.JWT_SECRET,
210
+ expiresIn: Bun.env.JWT_EXPIRES_IN,
211
+ },
212
+ databasePath: Bun.env.DATABASE_PATH,
213
+ logLevel: Bun.env.LOG_LEVEL,
214
+ corsOrigins: Bun.env.CORS_ORIGINS,
215
+ rateLimitMax: Bun.env.RATE_LIMIT_MAX_REQUESTS,
216
+ rateLimitWindowMs: Bun.env.RATE_LIMIT_WINDOW_MS,
217
+ });
218
+
219
+ if (!result.success) {
220
+ const errors = result.error.issues
221
+ .map((i) => \` \u2717 \${i.path.join(".")} \u2192 \${i.message}\`)
222
+ .join("\\n");
223
+
224
+ console.error(\`\\n CONFIG ERROR Invalid configuration\\n\\n\${errors}\\n\`);
225
+ console.error(" Hint: Copy .env.example to .env and set the required values:\\n");
226
+ console.error(" $ cp .env.example .env\\n");
227
+ process.exit(1);
228
+ }
229
+
230
+ return result.data;
231
+ };
232
+ `},{path:"src/database.ts",content:['import { Database } from "bun:sqlite";','import { existsSync, mkdirSync } from "node:fs";','import { dirname } from "node:path";','import type { Logger } from "./logger.js";',"","export const createDatabase = (dbPath: string, log: Logger): Database => {"," const dir = dirname(dbPath);"," if (!existsSync(dir)) mkdirSync(dir, { recursive: true });",""," const db = new Database(dbPath, { create: true });","",' db.exec("PRAGMA journal_mode = WAL");',' db.exec("PRAGMA synchronous = NORMAL");',' db.exec("PRAGMA foreign_keys = ON");',""," migrate(db, log, dbPath);"," return db;","};","","const migrate = (db: Database, log: Logger, dbPath: string): void => {"," db.exec(`"," CREATE TABLE IF NOT EXISTS users ("," id TEXT PRIMARY KEY,"," email TEXT NOT NULL UNIQUE,"," password TEXT NOT NULL,"," name TEXT NOT NULL DEFAULT '',"," role TEXT NOT NULL DEFAULT 'user',"," created_at TEXT NOT NULL DEFAULT (datetime('now')),"," updated_at TEXT NOT NULL DEFAULT (datetime('now'))"," )"," `);",""," db.exec(`"," CREATE TABLE IF NOT EXISTS token_blacklist ("," token_id TEXT PRIMARY KEY,"," expires_at TEXT NOT NULL"," )"," `);","",' log.info("SQLite database ready", { path: dbPath });',"};",""].join(`
233
+ `)},{path:"src/server.ts",content:['import type { AppConfig } from "./config.js";','import type { Logger } from "./logger.js";','import { formatRequest } from "./logger.js";','import type { Router } from "./router.js";','import type { TokenService } from "./utils/token.js";','import { authenticate } from "./middleware/auth.js";','import { json } from "./utils/response.js";',"","interface ServerDeps {"," config: AppConfig;"," router: Router;"," tokenService: TokenService;"," log: Logger;","}","","export const createServer = (deps: ServerDeps) => {"," const { config, router, tokenService, log } = deps;",""," // \u2500\u2500 Rate limiting (in-memory) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"," const hits = new Map<string, { count: number; resetAt: number }>();",""," const isRateLimited = (ip: string): boolean => {"," const now = Date.now();"," const entry = hits.get(ip);"," if (!entry || now > entry.resetAt) {"," hits.set(ip, { count: 1, resetAt: now + config.rateLimitWindowMs });"," return false;"," }"," entry.count++;"," return entry.count > config.rateLimitMax;"," };",""," // \u2500\u2500 CORS headers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"," const corsHeaders = (origin: string | null): Record<string, string> => {",' const allowed = config.corsOrigins === "*" || (origin && config.corsOrigins.includes(origin));'," return {",' "Access-Control-Allow-Origin": allowed ? (origin ?? "*") : "",',' "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",',' "Access-Control-Allow-Headers": "Content-Type, Authorization",',' "Access-Control-Max-Age": "86400",'," };"," };",""," // \u2500\u2500 Server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"," const server = Bun.serve({"," port: config.port,"," hostname: config.host,",""," async fetch(req) {"," const start = performance.now();"," const url = new URL(req.url);"," const { pathname } = url;"," const method = req.method;",' const origin = req.headers.get("origin");',' const ip = server.requestIP(req)?.address ?? "unknown";',""," // CORS preflight",' if (method === "OPTIONS") {'," return new Response(null, { status: 204, headers: corsHeaders(origin) });"," }",""," // Rate limiting"," if (isRateLimited(ip)) {"," const dur = performance.now() - start;"," console.log(formatRequest(method, pathname, 429, dur, ip));",' return json({ error: "Too many requests" }, 429, corsHeaders(origin));'," }",""," try {"," const match = router.match(method, pathname);",""," if (!match) {"," const dur = performance.now() - start;"," console.log(formatRequest(method, pathname, 404, dur, ip));",' return json({ error: "Not found" }, 404, corsHeaders(origin));'," }",""," // Auth check"," if (match.auth) {"," const authResult = await authenticate(req, tokenService);"," if (!authResult.ok) {"," const dur = performance.now() - start;"," console.log(formatRequest(method, pathname, 401, dur, ip));"," return json({ error: authResult.error }, 401, corsHeaders(origin));"," }"," // Inject userId into request via header",' req.headers.set("x-user-id", authResult.userId);'," }",""," const response = await match.handler(req);",""," // Add CORS headers"," const headers = corsHeaders(origin);"," for (const [k, v] of Object.entries(headers)) {"," response.headers.set(k, v);"," }",""," const dur = performance.now() - start;"," console.log(formatRequest(method, pathname, response.status, dur, ip));"," return response;"," } catch (err) {",' log.error("Unhandled error", { path: pathname, error: String(err) });'," const dur = performance.now() - start;"," console.log(formatRequest(method, pathname, 500, dur, ip));",' return json({ error: "Internal server error" }, 500, corsHeaders(origin));'," }"," },"," });",""," return server;","};",""].join(`
234
+ `)},{path:"src/handlers/health.handler.ts",content:`import { json } from "../utils/response.js";
235
+
236
+ export const healthHandler = () => ({
237
+ check: (): Response =>
238
+ json({ status: "ok", uptime: process.uptime() }),
239
+ });
240
+ `},{path:"src/handlers/auth.handler.ts",content:`import type { AuthService } from "../services/auth.service.js";
241
+ import { json } from "../utils/response.js";
242
+ import { z } from "zod";
243
+
244
+ const registerSchema = z.object({
245
+ email: z.string().email(),
246
+ password: z.string().min(8),
247
+ name: z.string().min(1).optional(),
248
+ });
249
+
250
+ const loginSchema = z.object({
251
+ email: z.string().email(),
252
+ password: z.string(),
253
+ });
254
+
255
+ export const authHandlers = (authService: AuthService) => ({
256
+ register: async (req: Request): Promise<Response> => {
257
+ const body = registerSchema.safeParse(await req.json());
258
+ if (!body.success) return json({ error: body.error.issues }, 400);
259
+
260
+ const result = await authService.register(body.data);
261
+ if (!result.ok) return json({ error: result.error }, 409);
262
+
263
+ return json({ data: result.data }, 201);
264
+ },
265
+
266
+ login: async (req: Request): Promise<Response> => {
267
+ const body = loginSchema.safeParse(await req.json());
268
+ if (!body.success) return json({ error: body.error.issues }, 400);
269
+
270
+ const result = await authService.login(body.data.email, body.data.password);
271
+ if (!result.ok) return json({ error: result.error }, 401);
272
+
273
+ return json({ data: result.data });
274
+ },
275
+
276
+ logout: async (req: Request): Promise<Response> => {
277
+ const header = req.headers.get("authorization") ?? "";
278
+ const token = header.replace("Bearer ", "");
279
+
280
+ await authService.logout(token);
281
+ return json({ data: { message: "Logged out" } });
282
+ },
283
+ });
284
+ `},{path:"src/handlers/user.handler.ts",content:['import type { UserService } from "../services/user.service.js";','import { json } from "../utils/response.js";','import { z } from "zod";',"","const updateSchema = z.object({"," name: z.string().min(1).optional(),"," email: z.string().email().optional(),","});","","export const userHandlers = (userService: UserService) => ({"," getProfile: async (req: Request): Promise<Response> => {",' const userId = req.headers.get("x-user-id") ?? "";'," const user = userService.findById(userId);",' if (!user) return json({ error: "User not found" }, 404);',""," const { password: _, ...profile } = user;"," return json({ data: profile });"," },",""," updateProfile: async (req: Request): Promise<Response> => {",' const userId = req.headers.get("x-user-id") ?? "";'," const body = updateSchema.safeParse(await req.json());"," if (!body.success) return json({ error: body.error.issues }, 400);",""," const updated = userService.update(userId, body.data);",' if (!updated) return json({ error: "User not found" }, 404);',""," const { password: _, ...profile } = updated;"," return json({ data: profile });"," },",""," deleteAccount: async (req: Request): Promise<Response> => {",' const userId = req.headers.get("x-user-id") ?? "";'," const deleted = userService.delete(userId);",' if (!deleted) return json({ error: "User not found" }, 404);',"",' return json({ data: { message: "Account deleted" } });'," },","});",""].join(`
285
+ `)},{path:"src/middleware/auth.ts",content:`import type { TokenService } from "../utils/token.js";
286
+
287
+ type AuthResult =
288
+ | { ok: true; userId: string }
289
+ | { ok: false; error: string };
290
+
291
+ export const authenticate = async (
292
+ req: Request,
293
+ tokenService: TokenService,
294
+ ): Promise<AuthResult> => {
295
+ const header = req.headers.get("authorization");
296
+ if (!header?.startsWith("Bearer ")) {
297
+ return { ok: false, error: "Missing or invalid Authorization header" };
298
+ }
299
+
300
+ const token = header.slice(7);
301
+ const payload = tokenService.verify(token);
302
+
303
+ if (!payload) {
304
+ return { ok: false, error: "Invalid or expired token" };
305
+ }
306
+
307
+ if (tokenService.isBlacklisted(payload.jti)) {
308
+ return { ok: false, error: "Token has been revoked" };
309
+ }
310
+
311
+ return { ok: true, userId: payload.sub };
312
+ };
313
+ `},{path:"src/services/auth.service.ts",content:`import type { Database } from "bun:sqlite";
314
+ import type { PasswordHasher } from "../utils/password.js";
315
+ import type { TokenService } from "../utils/token.js";
316
+
317
+ type Result<T> = { ok: true; data: T } | { ok: false; error: string };
318
+
319
+ interface RegisterInput {
320
+ email: string;
321
+ password: string;
322
+ name?: string;
323
+ }
324
+
325
+ export interface AuthService {
326
+ register(input: RegisterInput): Promise<Result<{ id: string; email: string; token: string }>>;
327
+ login(email: string, password: string): Promise<Result<{ token: string; user: { id: string; email: string; name: string } }>>;
328
+ logout(token: string): Promise<void>;
329
+ }
330
+
331
+ export const createAuthService = (
332
+ db: Database,
333
+ passwordHasher: PasswordHasher,
334
+ tokenService: TokenService,
335
+ ): AuthService => ({
336
+ async register(input) {
337
+ const existing = db.query("SELECT id FROM users WHERE email = ?").get(input.email);
338
+ if (existing) return { ok: false, error: "Email already registered" };
339
+
340
+ const id = crypto.randomUUID();
341
+ const hashedPassword = await passwordHasher.hash(input.password);
342
+
343
+ db.query("INSERT INTO users (id, email, password, name) VALUES (?, ?, ?, ?)").run(
344
+ id, input.email, hashedPassword, input.name ?? "",
345
+ );
346
+
347
+ const token = tokenService.sign({ sub: id, email: input.email, role: "user" });
348
+ return { ok: true, data: { id, email: input.email, token } };
349
+ },
350
+
351
+ async login(email, password) {
352
+ const user = db
353
+ .query("SELECT id, email, password, name, role FROM users WHERE email = ?")
354
+ .get(email) as { id: string; email: string; password: string; name: string; role: string } | null;
355
+
356
+ if (!user) return { ok: false, error: "Invalid credentials" };
357
+
358
+ const valid = await passwordHasher.verify(user.password, password);
359
+ if (!valid) return { ok: false, error: "Invalid credentials" };
360
+
361
+ const token = tokenService.sign({ sub: user.id, email: user.email, role: user.role });
362
+ return {
363
+ ok: true,
364
+ data: { token, user: { id: user.id, email: user.email, name: user.name } },
365
+ };
366
+ },
367
+
368
+ async logout(token) {
369
+ const payload = tokenService.verify(token);
370
+ if (payload?.jti) {
371
+ tokenService.blacklist(payload.jti, payload.exp);
372
+ }
373
+ },
374
+ });
375
+ `},{path:"src/services/user.service.ts",content:['import type { Database } from "bun:sqlite";','import type { PasswordHasher } from "../utils/password.js";',"","interface User {"," id: string;"," email: string;"," password: string;"," name: string;"," role: string;"," created_at: string;"," updated_at: string;","}","","export interface UserService {"," findById(id: string): User | null;"," update(id: string, data: { name?: string; email?: string }): User | null;"," delete(id: string): boolean;","}","","export const createUserService = (db: Database, _passwordHasher: PasswordHasher): UserService => ({"," findById(id) {",' return db.query("SELECT * FROM users WHERE id = ?").get(id) as User | null;'," },",""," update(id, data) {"," const fields: string[] = [];"," const values: unknown[] = [];",""," if (data.name !== undefined) {",' fields.push("name = ?");'," values.push(data.name);"," }"," if (data.email !== undefined) {",' fields.push("email = ?");'," values.push(data.email);"," }",""," if (fields.length === 0) return this.findById(id);","",` fields.push("updated_at = datetime('now')");`," values.push(id);","",' db.query("UPDATE users SET " + fields.join(", ") + " WHERE id = ?").run(...values);'," return this.findById(id);"," },",""," delete(id) {",' const result = db.query("DELETE FROM users WHERE id = ?").run(id);'," return result.changes > 0;"," },","});",""].join(`
376
+ `)},{path:"src/utils/password.ts",content:`/**
377
+ * Password hashing using Bun's built-in Argon2id.
378
+ */
379
+ export interface PasswordHasher {
380
+ hash(password: string): Promise<string>;
381
+ verify(hash: string, password: string): Promise<boolean>;
382
+ }
383
+
384
+ export const createPasswordHasher = (): PasswordHasher => ({
385
+ async hash(password) {
386
+ return Bun.password.hash(password, { algorithm: "argon2id" });
387
+ },
388
+
389
+ async verify(hash, password) {
390
+ return Bun.password.verify(password, hash);
391
+ },
392
+ });
393
+ `},{path:"src/utils/token.ts",content:["/**"," * JWT token service using Bun's native HMAC-SHA256."," */","","export interface TokenPayload {"," sub: string;"," jti: string;"," exp: number;"," [key: string]: unknown;","}","","export interface TokenService {"," sign(claims: Record<string, unknown>): string;"," verify(token: string): TokenPayload | null;"," blacklist(jti: string, exp: number): void;"," isBlacklisted(jti: string): boolean;","}","",'// Parse duration string to seconds: "15m" \u2192 900, "1h" \u2192 3600, "7d" \u2192 604800',"const parseDuration = (duration: string): number => {"," const match = duration.match(/^(\\d+)([smhd])$/);"," if (!match) return 900; // default 15 minutes"," const value = Number.parseInt(match[1], 10);"," const unit = match[2];"," const multipliers: Record<string, number> = { s: 1, m: 60, h: 3600, d: 86400 };"," return value * (multipliers[unit] ?? 60);","};","","export const createTokenService = (secret: string, expiresIn: string): TokenService => {"," const key = new TextEncoder().encode(secret);"," const expiresInSec = parseDuration(expiresIn);",""," // In-memory blacklist with auto-cleanup"," const blacklisted = new Map<string, number>();",""," // Clean expired entries every 5 minutes"," setInterval(() => {"," const now = Math.floor(Date.now() / 1000);"," for (const [jti, exp] of blacklisted) {"," if (exp < now) blacklisted.delete(jti);"," }"," }, 5 * 60 * 1000).unref();",""," return {"," sign(claims) {"," const now = Math.floor(Date.now() / 1000);"," const jti = crypto.randomUUID();"," const payload = { ...claims, jti, iat: now, exp: now + expiresInSec };",""," // HMAC-SHA256 JWT",' const header = btoa(JSON.stringify({ alg: "HS256", typ: "JWT" }))',' .replace(/=/g, "").replace(/\\+/g, "-").replace(/\\//g, "_");',""," const body = btoa(JSON.stringify(payload))",' .replace(/=/g, "").replace(/\\+/g, "-").replace(/\\//g, "_");',"",' const data = header + "." + body;',' const hmac = new Bun.CryptoHasher("sha256", key);'," hmac.update(data);",' const sig = Buffer.from(hmac.digest()).toString("base64url");',"",' return data + "." + sig;'," },",""," verify(token) {"," try {",' const parts = token.split(".");'," if (parts.length !== 3) return null;",""," // Verify signature",' const data = parts[0] + "." + parts[1];',' const hmac = new Bun.CryptoHasher("sha256", key);'," hmac.update(data);",' const expected = Buffer.from(hmac.digest()).toString("base64url");',""," if (expected !== parts[2]) return null;",""," // Decode payload"," const payload = JSON.parse(",' Buffer.from(parts[1], "base64url").toString()'," ) as TokenPayload;",""," // Check expiry"," if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {"," return null;"," }",""," return payload;"," } catch {"," return null;"," }"," },",""," blacklist(jti, exp) {"," blacklisted.set(jti, exp);"," },",""," isBlacklisted(jti) {"," return blacklisted.has(jti);"," },"," };","};",""].join(`
394
+ `)},{path:"src/utils/response.ts",content:`/**
395
+ * JSON response helper.
396
+ */
397
+ export const json = (
398
+ body: unknown,
399
+ status = 200,
400
+ extraHeaders?: Record<string, string>,
401
+ ): Response => {
402
+ const headers: Record<string, string> = {
403
+ "Content-Type": "application/json; charset=utf-8",
404
+ ...extraHeaders,
405
+ };
406
+
407
+ return new Response(JSON.stringify(body), { status, headers });
408
+ };
409
+ `},{path:"tests/health.test.ts",content:`import { describe, expect, it } from "bun:test";
410
+
411
+ describe("Health check", () => {
412
+ it("should return ok", async () => {
413
+ const response = await fetch("http://localhost:3000/health");
414
+ expect(response.status).toBe(200);
415
+
416
+ const data = await response.json();
417
+ expect(data.status).toBe("ok");
418
+ });
419
+ });
420
+ `}];var DJ=/^[a-zA-Z0-9_-]+$/,xJ=(J)=>{if(!J)return"Project name is required.";if(!DJ.test(J))return"Project name can only contain letters, numbers, hyphens, and underscores.";if(J.length>214)return"Project name is too long (max 214 chars).";return null},c=async(J,$=process.cwd())=>{let K=Bun.spawn(J,{cwd:$,stdout:"pipe",stderr:"pipe"}),[W,G]=await Promise.all([new Response(K.stdout).text(),new Response(K.stderr).text()]),z=await K.exited;return{stdout:W.trim(),stderr:G.trim(),exitCode:z}},PJ=async(J)=>{try{let{exitCode:$}=await c(["which",J]);return $===0}catch{return!1}},YJ=async(J,$)=>{let K=performance.now();Z(),X(g($)),Z();let W=J[0]??"",G=J.includes("--cwd")||J.includes(".");if(!W&&!G)W=await zJ("Project name","my-api");if(G)W=".";if(W!=="."){let U=xJ(W);if(U)v(U),process.exit(1)}let z=W==="."?process.cwd():NJ(process.cwd(),W);if(W!=="."&&t(z)){if((await Array.fromAsync(new Bun.Glob("*").scan({cwd:z}))).length>0){if(!await m(`Directory ${H(E(W))} already exists and is not empty. Continue?`,!1))C("Aborted."),process.exit(0)}}if(V("Creating project"),W!==".")qJ(z,{recursive:!0}),b(`Created directory ${H(q(W))}`);let w=y("Generating project files...");w.start();let D=W==="."?"my-api":W,Y=KJ(D);for(let U of Y){let T=QJ(z,U.path),O=VJ(T);if(!t(O))qJ(O,{recursive:!0});CJ(T,U.content,"utf-8")}w.stop(`Generated ${H(q(String(Y.length)))} files`);let L=QJ(z,".env.example"),S=QJ(z,".env");if(t(L)&&!t(S))try{let U=await Bun.file(L).text(),T=HJ(64);U=U.replace("change-me-to-a-64-char-random-string",T),await Bun.write(S,U),b(`Generated ${H(q(".env"))} with secure JWT_SECRET`)}catch{k("Could not generate .env \u2014 copy .env.example manually")}V("Installing dependencies");let _=y("Running bun install...");_.start();let{exitCode:h,stderr:o}=await c(["bun","install"],z);if(h!==0)_.stop(),v("Failed to install dependencies:"),X(` ${Q(o)}`),Z(),C(`Run ${H(q("bun install"))} manually in the project directory.`);else _.stop("Dependencies installed");if(await PJ("git"))await c(["git","init"],z),await c(["git","add","-A"],z),await c(["git","commit","-m","Initial commit from onlyApi CLI","--no-verify"],z),b("Initialized git repository");let a=performance.now()-K;Z(),X(` ${I.rocket} ${H(R("Project created successfully!"))} ${Q(`(${i(a)})`)}`),Z(),V("Project structure"),Z();let s=[`${H(q(D))}/`,"\u251C\u2500\u2500 src/",`\u2502 \u251C\u2500\u2500 main.ts ${Q("\u2190 entry point")}`,`\u2502 \u251C\u2500\u2500 config.ts ${Q("\u2190 env config")}`,`\u2502 \u251C\u2500\u2500 database.ts ${Q("\u2190 SQLite + migrations")}`,`\u2502 \u251C\u2500\u2500 logger.ts ${Q("\u2190 colored structured logger")}`,`\u2502 \u251C\u2500\u2500 router.ts ${Q("\u2190 route table + matching")}`,`\u2502 \u251C\u2500\u2500 server.ts ${Q("\u2190 HTTP server + middleware")}`,"\u2502 \u251C\u2500\u2500 handlers/",`\u2502 \u2502 \u251C\u2500\u2500 auth.handler.ts ${Q("\u2190 register/login/logout")}`,"\u2502 \u2502 \u251C\u2500\u2500 health.handler.ts",`\u2502 \u2502 \u2514\u2500\u2500 user.handler.ts ${Q("\u2190 profile CRUD")}`,"\u2502 \u251C\u2500\u2500 middleware/",`\u2502 \u2502 \u2514\u2500\u2500 auth.ts ${Q("\u2190 JWT guard")}`,"\u2502 \u251C\u2500\u2500 services/","\u2502 \u2502 \u251C\u2500\u2500 auth.service.ts","\u2502 \u2502 \u2514\u2500\u2500 user.service.ts","\u2502 \u2514\u2500\u2500 utils/",`\u2502 \u251C\u2500\u2500 password.ts ${Q("\u2190 Argon2id")}`,`\u2502 \u251C\u2500\u2500 token.ts ${Q("\u2190 JWT sign/verify")}`,"\u2502 \u2514\u2500\u2500 response.ts","\u251C\u2500\u2500 tests/","\u251C\u2500\u2500 Dockerfile",`\u251C\u2500\u2500 .env ${Q("\u2190 auto-generated")}`,"\u2514\u2500\u2500 package.json"];for(let U of s)X(` ${U}`);Z(),V("Next steps"),Z();let B=W!=="."?`cd ${W}`:null,F=[...B?[B]:[],"bun run dev # Start dev server (hot-reload)","bun test # Run tests","bun run check # Type-check"];for(let U of F)X(` ${Q("$")} ${H(q(U))}`);Z(),V("API endpoints"),Z(),X(` ${Q("GET")} /health ${Q("\u2190 health check")}`),X(` ${Q("POST")} /api/v1/auth/register ${Q("\u2190 create account")}`),X(` ${Q("POST")} /api/v1/auth/login ${Q("\u2190 get JWT token")}`),X(` ${Q("POST")} /api/v1/auth/logout ${Q("\u2190 revoke token")} ${Q("\uD83D\uDD12")}`),X(` ${Q("GET")} /api/v1/users/me ${Q("\u2190 get profile")} ${Q("\uD83D\uDD12")}`),X(` ${Q("PATCH")} /api/v1/users/me ${Q("\u2190 update profile")} ${Q("\uD83D\uDD12")}`),X(` ${Q("DELETE")} /api/v1/users/me ${Q("\u2190 delete account")} ${Q("\uD83D\uDD12")}`),Z(),X(` ${Q("Docs:")} ${q("https://github.com/lysari/onlyapi#readme")}`),X(` ${Q("Issues:")} ${q("https://github.com/lysari/onlyapi/issues")}`),Z(),X(` ${Q("Happy hacking!")} ${I.bolt}`),Z()};import{existsSync as N}from"fs";import{join as P,resolve as wJ}from"path";var BJ="https://api.github.com/repos/lysari/onlyapi",SJ=(J)=>`https://github.com/lysari/onlyapi/archive/refs/tags/${J}.tar.gz`,GJ="https://github.com/lysari/onlyapi/archive/refs/heads/main.tar.gz",hJ="https://registry.npmjs.org/only-api",fJ=["src/core/errors/app-error.ts","src/core/types/brand.ts","src/core/types/result.ts","src/infrastructure/logging/logger.ts","src/infrastructure/security/password-hasher.ts","src/infrastructure/security/token-service.ts","src/presentation/middleware/cors.ts","src/presentation/middleware/rate-limit.ts","src/presentation/middleware/security-headers.ts","src/presentation/server.ts","src/presentation/context.ts","src/shared/cli.ts","src/shared/container.ts","src/shared/utils/id.ts","src/shared/utils/timing-safe.ts","src/shared/log-format.ts","src/cluster.ts","tsconfig.json","biome.json"],d=async(J,$=process.cwd())=>{let K=Bun.spawn(J,{cwd:$,stdout:"pipe",stderr:"pipe"}),[W,G]=await Promise.all([new Response(K.stdout).text(),new Response(K.stderr).text()]),z=await K.exited;return{stdout:W.trim(),stderr:G.trim(),exitCode:z}},UJ=(J)=>{let $=J.replace(/^v/,"").split(".").map(Number);return[$[0]??0,$[1]??0,$[2]??0]},FJ=(J,$)=>{let[K,W,G]=UJ(J),[z,w,D]=UJ($);if(K!==z)return K>z;if(W!==w)return W>w;return G>D},jJ=async()=>{try{let J=await fetch(`${BJ}/releases/latest`,{headers:{Accept:"application/vnd.github.v3+json"}});if(J.ok)return(await J.json()).tag_name.replace(/^v/,"")}catch{}try{let J=await fetch(`${BJ}/tags?per_page=1`,{headers:{Accept:"application/vnd.github.v3+json"}});if(J.ok){let $=await J.json();if($.length>0){let K=$[0];return K?K.name.replace(/^v/,""):null}}}catch{}try{let J=await fetch(hJ);if(J.ok)return(await J.json())["dist-tags"].latest}catch{}return null},TJ=async(J,$)=>{let K=performance.now(),W=wJ(process.cwd());Z(),X(g($)),Z();let G=P(W,"package.json");if(!N(G))v("No package.json found in current directory."),C("Run this command from the root of your onlyApi project."),process.exit(1);let z;try{z=JSON.parse(await Bun.file(G).text()).version??"0.0.0"}catch{v("Could not read package.json."),process.exit(1)}if(!(N(P(W,"src/main.ts"))&&N(P(W,"src/core"))&&N(P(W,"src/presentation"))))v("This doesn't appear to be an onlyApi project."),C("Expected to find src/main.ts, src/core/, and src/presentation/"),process.exit(1);V("Checking for updates");let D=y("Fetching latest version...");D.start();let Y=await jJ();if(!Y){if(D.stop(),k("Could not determine the latest version."),C("This may be due to network issues or API rate limits."),Z(),!(J.includes("--force")||J.includes("-f"))){if(!await m("Continue with upgrade from main branch?",!1))C("Aborted."),process.exit(0)}}else{if(D.stop("Version check complete"),Z(),e([["Current version",z],["Latest version",Y]]),Z(),!FJ(Y,z)&&!J.includes("--force")&&!J.includes("-f"))n("You're already on the latest version!"),Z(),process.exit(0);if(FJ(Y,z))C(`Update available: ${H(j(z))} ${Q("\u2192")} ${H(R(Y))}`);else C(`Re-applying latest version ${Q("(--force)")}`)}let L=N(P(W,".git"));if(L){let{stdout:B}=await d(["git","status","--porcelain"],W);if(B){if(Z(),k("You have uncommitted changes."),!await m("Continue anyway?",!1))C("Commit your changes first, then retry."),process.exit(0)}}V("Downloading update");let S=y("Downloading latest source...");S.start();let _=P(W,".onlyapi-upgrade-tmp");try{if(N(_)){let{rmSync:LJ}=await import("fs");LJ(_,{recursive:!0,force:!0})}let{mkdirSync:B}=await import("fs");B(_,{recursive:!0});let F=Y?SJ(`v${Y}`):GJ,U=await fetch(F),T=U;if(!U.ok&&Y)S.update("Tag not found, trying main branch..."),T=await fetch(GJ);if(!T.ok)throw Error(`HTTP ${T.status}`);let O=P(_,"update.tar.gz");await Bun.write(O,T),S.update("Extracting..."),await d(["tar","xzf",O,"--strip-components=1"],_);let{rmSync:r}=await import("fs");r(O,{force:!0}),S.stop("Download complete")}catch(B){if(S.stop(),v(`Failed to download update: ${B instanceof Error?B.message:String(B)}`),N(_)){let{rmSync:F}=await import("fs");F(_,{recursive:!0,force:!0})}process.exit(1)}V("Applying updates");let h=0,o=0,f=J.includes("--dry-run");for(let B of fJ){let F=P(_,B),U=P(W,B);if(!N(F))continue;try{let T=await Bun.file(F).text();if(N(U)){if(await Bun.file(U).text()===T){o++;continue}}if(!f){let O=U.substring(0,U.lastIndexOf("/")),{mkdirSync:r}=await import("fs");r(O,{recursive:!0}),await Bun.write(U,T)}b(`${f?`${Q("[dry-run]")} `:""}Updated ${H(q(B))}`),h++}catch{k(`Could not update ${B}`)}}let a=P(_,"package.json");if(N(a))try{let B=JSON.parse(await Bun.file(a).text()),F=JSON.parse(await Bun.file(G).text()),U=!1;if(B.dependencies){F.dependencies=F.dependencies??{};for(let[T,O]of Object.entries(B.dependencies))if(F.dependencies[T]!==O)F.dependencies[T]=O,U=!0}if(B.devDependencies){F.devDependencies=F.devDependencies??{};for(let[T,O]of Object.entries(B.devDependencies))if(F.devDependencies[T]!==O)F.devDependencies[T]=O,U=!0}if(Y)F.version=Y;if(!f)await Bun.write(G,`${JSON.stringify(F,null,2)}
421
+ `);if(U)b(`${f?`${Q("[dry-run]")} `:""}Updated dependencies in ${H(q("package.json"))}`)}catch{k("Could not merge package.json dependencies")}if(N(_)){let{rmSync:B}=await import("fs");B(_,{recursive:!0,force:!0})}if(!f&&h>0){V("Installing dependencies");let B=y("Running bun install...");B.start();let{exitCode:F}=await d(["bun","install"],W);if(F!==0)B.stop(),k("bun install failed \u2014 run it manually.");else B.stop("Dependencies installed")}if(L&&!f&&h>0){if(await m("Create a git commit for this upgrade?")){let F=Y?`chore: upgrade onlyApi to v${Y}`:"chore: upgrade onlyApi to latest";await d(["git","add","-A"],W),await d(["git","commit","-m",F,"--no-verify"],W),b("Created upgrade commit")}}let s=performance.now()-K;if(Z(),h>0)X(` ${I.rocket} ${H(R("Upgrade complete!"))} ${Q(`(${i(s)})`)}`),Z(),e([["Files updated",String(h)],["Files unchanged",String(o)]]);else if(f)X(` ${I.info} ${H(q("Dry run complete"))} \u2014 no files were modified.`);else n("All files are already up to date!");if(Z(),h>0)X(` ${Q("Note: The following files are NOT auto-upgraded (your custom code):")}`),X(` ${Q(" - src/application/ (your services & DTOs)")}`),X(` ${Q(" - src/core/entities/ (your domain entities)")}`),X(` ${Q(" - src/core/ports/ (your port interfaces)")}`),X(` ${Q(" - src/presentation/handlers/ (your route handlers)")}`),X(` ${Q(" - src/presentation/routes/ (your routes)")}`),X(` ${Q(" - src/main.ts (your bootstrap)")}`),Z(),X(` ${Q("Review the")} ${q("CHANGELOG.md")} ${Q("for breaking changes.")}`),Z()};var l="1.7.0",IJ=process.argv.slice(2),_J=IJ[0]?.toLowerCase()??"",OJ=IJ.slice(1),kJ=async()=>{try{switch(_J){case"init":case"create":case"new":await YJ(OJ,l);break;case"upgrade":case"update":await TJ(OJ,l);break;case"version":case"-v":case"--version":X(`onlyapi v${l}`);break;case"help":case"-h":case"--help":JJ(l);break;case"":JJ(l);break;default:Z(),v(`Unknown command: ${H(E(_J))}`),Z(),X(` ${Q("Run")} ${q("onlyapi help")} ${Q("to see available commands.")}`),Z(),process.exit(1)}}catch(J){Z(),v(J instanceof Error?J.message:String(J)),Z(),process.exit(1)}};kJ();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enderworld/onlyapi",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "description": "Zero-dependency, enterprise-grade REST API built on Bun — fastest runtime, strictest TypeScript, cleanest architecture.",
5
5
  "type": "module",
6
6
  "license": "MIT",