rapidkit 0.21.2 → 0.23.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/README.md +75 -11
- package/dist/chunk-74G6C57B.js +24 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3452 -201
- package/dist/package.json +3 -2
- package/dist/pythonRapidkitExec-YIFUZLND.js +1 -0
- package/package.json +3 -2
- package/scripts/enforce-package-manager.cjs +10 -0
- package/dist/chunk-D2ZRDZOE.js +0 -17
- package/dist/pythonRapidkitExec-GFCAVUOY.js +0 -1
package/dist/index.js
CHANGED
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {d,c as c$1,
|
|
2
|
+
import {e,d,c as c$1,h,a as a$1}from'./chunk-74G6C57B.js';import {d as d$1,b as b$1}from'./chunk-RV6HBTFC.js';import {c,a,b}from'./chunk-7LU4Z66R.js';import {Command,Option}from'commander';import g from'chalk';import ae from'inquirer';import v from'path';import {fileURLToPath,pathToFileURL}from'url';import {exec,spawn}from'child_process';import A,{promises}from'fs';import*as qo from'os';import qo__default from'os';import Ko from'validate-npm-package-name';import*as R from'fs-extra';import R__default from'fs-extra';import {execa}from'execa';import pt from'ora';import nr from'nunjucks';import sr,{createHash}from'crypto';import {promisify}from'util';var Fo=".rapidkitrc.json",Ho=["rapidkit.config.js","rapidkit.config.mjs","rapidkit.config.cjs"];async function Z(){let e=v.join(qo__default.homedir(),Fo);try{let o=await promises.readFile(e,"utf-8"),t=JSON.parse(o);return a.debug(`Loaded config from ${e}`),t}catch{return a.debug("No user config found, using defaults"),{}}}async function Nt(e=process.cwd()){let o=e,t=v.parse(o).root;for(;o!==t;){for(let r of Ho){let i=v.join(o,r);try{await promises.access(i),a.debug(`Found config file: ${i}`);let s=await import(pathToFileURL(i).href),c=s.default||s;return a.debug(`Loaded RapidKit config from ${r}`),c}catch{continue}}o=v.dirname(o);}return a.debug("No RapidKit config file found, using defaults"),{}}function jt(e,o,t){return {author:t.author||o.workspace?.defaultAuthor||e.author,pythonVersion:o.workspace?.pythonVersion||e.pythonVersion,defaultInstallMethod:t.defaultInstallMethod||o.workspace?.installMethod||e.defaultInstallMethod,defaultKit:t.defaultKit||o.projects?.defaultKit||e.defaultKit,skipGit:t.skipGit??o.projects?.skipGit??e.skipGit,license:t.license||e.license,testRapidKitPath:t.testRapidKitPath||e.testRapidKitPath}}function Ae(e){return process.env.RAPIDKIT_DEV_PATH||e.testRapidKitPath||void 0}var H=class extends Error{constructor(t,r,i){super(t);this.code=r;this.details=i;this.name="RapidKitError",Error.captureStackTrace(this,this.constructor);}},de=class extends H{constructor(o,t){let r=t?`Python ${o}+ required, found ${t}`:`Python ${o}+ not found`;super(r,"PYTHON_NOT_FOUND","Please install Python from https://www.python.org/downloads/");}},we=class extends H{constructor(){super("Poetry is not installed","POETRY_NOT_FOUND","Install Poetry from https://python-poetry.org/docs/#installation");}},ye=class extends H{constructor(){super("pipx is not installed","PIPX_NOT_FOUND","Install pipx from https://pypa.github.io/pipx/installation/");}},Oe=class extends H{constructor(o){super(`Directory "${o}" already exists`,"DIRECTORY_EXISTS","Please choose a different name or remove the existing directory");}},Y=class extends H{constructor(o,t){super(`Invalid project name: "${o}"`,"INVALID_PROJECT_NAME",t);}},F=class extends H{constructor(o,t){let r=`Installation failed at: ${o}`,i=`${t.message}
|
|
3
3
|
|
|
4
4
|
Troubleshooting:
|
|
5
5
|
- Check your internet connection
|
|
6
6
|
- Verify Python/Poetry installation
|
|
7
|
-
- Try running with --debug flag for more details`;super(
|
|
7
|
+
- Try running with --debug flag for more details`;super(r,"INSTALLATION_ERROR",i);}},le=class extends H{constructor(){super("RapidKit Python package is not yet available on PyPI","RAPIDKIT_NOT_AVAILABLE",`Available options:
|
|
8
8
|
1. Install Python 3.10+ and retry the same command
|
|
9
9
|
2. Use the core workflow: npx rapidkit create workspace <name>
|
|
10
10
|
3. Offline fallback (limited): npx rapidkit create project fastapi.standard <name> --output .
|
|
11
11
|
|
|
12
|
-
Legacy: set RAPIDKIT_SHOW_LEGACY=1 to reveal template-mode flags in help.`);}};function
|
|
12
|
+
Legacy: set RAPIDKIT_SHOW_LEGACY=1 to reveal template-mode flags in help.`);}};function ct(e){let o=Ko(e);if(!o.validForNewPackages){let r=o.errors||[],i=o.warnings||[],n=[...r,...i];throw new Y(e,`NPM validation failed: ${n.join(", ")}`)}if(!/^[a-z][a-z0-9_-]*$/.test(e))throw new Y(e,"Must start with a lowercase letter and contain only lowercase letters, numbers, hyphens, and underscores");if(["test","tests","src","dist","build","lib","python","pip","poetry","node","npm","rapidkit","rapidkit"].includes(e.toLowerCase()))throw new Y(e,`"${e}" is a reserved name. Please choose a different name.`);if(e.length<2)throw new Y(e,"Name must be at least 2 characters long");if(e.length>214)throw new Y(e,"Name must be less than 214 characters");return true}function Vo(e){return typeof e=="object"&&e!==null}async function Wo(e,o,t,r=8e3){try{let i=await execa(e,o,{cwd:t,timeout:r,reject:false,stdio:"pipe"});return {ok:i.exitCode===0,exitCode:i.exitCode,stdout:i.stdout,stderr:i.stderr}}catch(i){return {ok:false,exitCode:void 0,stdout:"",stderr:i instanceof Error?i.message:String(i)}}}async function Bo(e,o){let t=["-m","rapidkit",...e],r=["python3","python"];for(let i of r){let n=await Wo(i,t,o?.cwd,o?.timeoutMs);if(!n.ok)continue;let s=(n.stdout??"").trim();try{let c=JSON.parse(s);return Vo(c)?{ok:true,command:i,exitCode:n.exitCode,stdout:n.stdout,stderr:n.stderr,data:c}:{ok:false,command:i,exitCode:n.exitCode,stdout:n.stdout,stderr:n.stderr}}catch{return {ok:false,command:i,exitCode:n.exitCode,stdout:n.stdout,stderr:n.stderr}}}return {ok:false}}async function Gt(e,o){let t=await Bo(["project","detect","--path",e,"--json"],o);return !t.ok||!t.data||t.data.schema_version!==1?{ok:false,command:t.command,exitCode:t.exitCode,stdout:t.stdout,stderr:t.stderr}:t}function Ne(){return process.platform==="win32"?"python":"python3"}async function Mt(e,o,t,r){let i=d$1(o,c(),t);r&&(i.metadata||(i.metadata={}),i.metadata.python={version:r}),await b$1(e,i);}async function qt(e){await R.outputFile(v.join(e,".gitignore"),`.venv/
|
|
13
13
|
__pycache__/
|
|
14
14
|
*.pyc
|
|
15
15
|
.env
|
|
16
16
|
.rapidkit-workspace/
|
|
17
17
|
|
|
18
|
-
`,"utf-8");}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
`,"utf-8");}function Jo(e,o,t){return JSON.stringify({schema_version:"1.0",workspace_name:e,rapidkit_version:c(),created_at:new Date().toISOString(),created_by:"rapidkit-npm",profile:"minimal",engine:{install_method:o,python_version:t||null}},null,2)}function zo(e,o){return JSON.stringify({schema_version:"1.0",generated_by:"rapidkit-npm",generated_at:new Date().toISOString(),runtime:{python:{version:o||null,install_method:e},node:{version:process.version},go:{version:null}}},null,2)}function Yo(){return `version: "1.0"
|
|
19
|
+
mode: warn
|
|
20
|
+
rules:
|
|
21
|
+
enforce_workspace_marker: true
|
|
22
|
+
enforce_toolchain_lock: false
|
|
23
|
+
disallow_untrusted_tool_sources: false
|
|
24
|
+
`}function Qo(){return `version: "1.0"
|
|
25
|
+
cache:
|
|
26
|
+
strategy: shared
|
|
27
|
+
prune_on_bootstrap: false
|
|
28
|
+
self_heal: true
|
|
29
|
+
verify_integrity: false
|
|
30
|
+
`}async function Lt(e,o,t,r){await R.outputFile(v.join(e,".rapidkit","workspace.json"),Jo(o,t,r),"utf-8"),await R.outputFile(v.join(e,".rapidkit","toolchain.lock"),zo(t,r),"utf-8"),await R.outputFile(v.join(e,".rapidkit","policies.yml"),Yo(),"utf-8"),await R.outputFile(v.join(e,".rapidkit","cache-config.yml"),Qo(),"utf-8");}async function $t(e){try{let{stdout:o}=await execa(e,["--version"],{timeout:3e3}),t=o.match(/Python (\d+\.\d+\.\d+)/);if(t)return t[1]}catch{}return null}async function Xo(e,o){try{await promises.writeFile(v.join(e,".python-version"),`${o}
|
|
31
|
+
`,"utf-8"),a.debug(`Created .python-version with ${o}`);}catch(t){a.warn(`Failed to create .python-version: ${t}`);}}function ve(){let e=v.join(qo.homedir(),".local","bin"),t=(process.env.PATH||"").split(v.delimiter).filter(Boolean);t.includes(e)||(process.env.PATH=[e,...t].join(v.delimiter));}async function je(e,o){ve(),e.start("Checking pipx installation");try{return await execa("pipx",["--version"]),e.succeed("pipx found"),{kind:"binary"}}catch{}let t=Ne();try{return await execa(t,["-m","pipx","--version"]),e.succeed("pipx found"),{kind:"python-module",pythonCmd:t}}catch{}if(o)throw new ye;let{installPipx:r}=await ae.prompt([{type:"confirm",name:"installPipx",message:"pipx is not installed. Install it now (user install via python -m pip)?",default:true}]);if(!r)throw new ye;e.start("Installing pipx (user install)");try{try{await execa(t,["-m","pip","install","--user","--upgrade","pip"]);}catch{}await execa(t,["-m","pip","install","--user","--upgrade","pipx"]);}catch(i){let n=i,s=String(n?.stderr||n?.shortMessage||n?.message||"");throw new F("Install pipx with python -m pip",i instanceof Error?i:new Error(s))}e.succeed("pipx installed"),ve();try{return await execa(t,["-m","pipx","--version"]),{kind:"python-module",pythonCmd:t}}catch(i){let n=i,s=String(n?.stderr||n?.shortMessage||n?.message||"pipx not runnable after install");throw new F("Verify pipx after install",new Error(`${s}
|
|
32
|
+
|
|
33
|
+
Try reopening your terminal or run: python3 -m pipx ensurepath`))}}async function pe(e,o){return e.kind==="binary"?execa("pipx",o):execa(e.pythonCmd,["-m","pipx",...o])}async function Zo(e,o){ve(),e.start("Checking Poetry installation");try{await execa("poetry",["--version"]),e.succeed("Poetry found");return}catch{}if(o)throw new we;let{installPoetry:t}=await ae.prompt([{type:"confirm",name:"installPoetry",message:"Poetry is not installed. Install it now using pipx?",default:true}]);if(!t)throw new we;let r=await je(e,o);e.start("Installing Poetry with pipx");try{await pe(r,["install","poetry"]);}catch(i){let n=i,s=String(n?.stderr||n?.shortMessage||n?.message||"");if(/already\s+installed|already\s+seems\s+to\s+be\s+installed|exists/i.test(s))try{await pe(r,["upgrade","poetry"]);}catch{}else throw new F("Install Poetry with pipx",i instanceof Error?i:new Error(s))}e.succeed("Poetry installed"),ve();try{await execa("poetry",["--version"]);}catch(i){let n=i,s=String(n?.stderr||n?.shortMessage||n?.message||"Poetry not found on PATH");throw new F("Verify Poetry after pipx install",new Error(`${s}
|
|
34
|
+
|
|
35
|
+
Poetry may be installed but not on PATH yet. Try reopening your terminal or run: pipx ensurepath`))}}function er(e){let o=e==="poetry";return `#!/usr/bin/env sh
|
|
24
36
|
set -eu
|
|
25
37
|
|
|
26
38
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
@@ -39,7 +51,7 @@ echo "- If you used venv: ensure .venv exists (or re-run the installer)." 1>&2
|
|
|
39
51
|
${o?`echo "- If you used Poetry: run 'poetry install' and retry, or activate the env." 1>&2
|
|
40
52
|
`:""}echo "Tip: you can also run: ./.venv/bin/rapidkit --help" 1>&2
|
|
41
53
|
exit 1
|
|
42
|
-
`}function
|
|
54
|
+
`}function tr(e){return `@echo off
|
|
43
55
|
setlocal
|
|
44
56
|
|
|
45
57
|
set "SCRIPT_DIR=%~dp0"
|
|
@@ -58,14 +70,14 @@ if %ERRORLEVEL%==0 if exist "%SCRIPT_DIR%\\pyproject.toml" (
|
|
|
58
70
|
`:""}echo RapidKit launcher could not find a local Python CLI. 1>&2
|
|
59
71
|
echo Tip: run .venv\\Scripts\\rapidkit.exe --help 1>&2
|
|
60
72
|
exit /b 1
|
|
61
|
-
`}async function
|
|
73
|
+
`}async function Ft(e,o){await R.outputFile(v.join(e,"rapidkit"),er(o),{encoding:"utf-8",mode:493}),await R.outputFile(v.join(e,"rapidkit.cmd"),tr(o),"utf-8");}async function Ge(e,o){let{skipGit:t=false,testMode:r=false,demoMode:i=false,dryRun:n=false,yes:s=false,userConfig:c={},installMethod:a$1}=o,l=e||"rapidkit",d=v.resolve(process.cwd(),l);if(await R.pathExists(d))throw new Oe(l);if(n){await rr(d,l,i,c);return}if(i){await or(d,l,t);return}let p=s?{pythonVersion:c.pythonVersion||"3.10",installMethod:a$1||c.defaultInstallMethod||"poetry"}:await ae.prompt([{type:"list",name:"pythonVersion",message:"Select Python version for RapidKit:",choices:["3.10","3.11","3.12"],default:c.pythonVersion||"3.10"},{type:"list",name:"installMethod",message:"How would you like to install RapidKit?",choices:[{name:"\u{1F3AF} Poetry (Recommended - includes virtual env)",value:"poetry"},{name:"\u{1F4E6} pip with venv (Standard)",value:"venv"},{name:"\u{1F527} pipx (Global isolated install)",value:"pipx"}],default:c.defaultInstallMethod||"poetry"}]);a.step(1,3,"Setting up RapidKit environment");let u=pt("Creating directory").start();try{await R.ensureDir(d),u.succeed("Directory created"),u.start("Detecting Python version");let h=null,x=await Ht(p.pythonVersion);if(x)h=await $t(x),h?(a.info(` Detected Python ${h}`),u.succeed(`Python ${h} detected`)):u.warn("Could not detect exact Python version");else {let y=Ne();h=await $t(y),h?u.succeed(`Python ${h} detected`):u.warn("Could not detect Python version, proceeding with defaults");}if(await Mt(d,l,p.installMethod,h||void 0),h&&await Xo(d,h),await Lt(d,l,p.installMethod,h||p.pythonVersion),await qt(d),p.installMethod==="poetry")try{await Kt(d,p.pythonVersion,u,r,c,s);}catch(y){let C=y?.details||y?.message||String(y);if(C.includes("pyenv")||C.includes("exit status 127")||C.includes("returned non-zero exit status 127")){u.warn("Poetry encountered Python discovery issues, trying venv method"),a.debug(`Poetry error (attempting venv fallback): ${C}`);try{await dt(d,p.pythonVersion,u,r,c),p.installMethod="venv";}catch(q){throw q}}else throw y}else p.installMethod==="venv"?await dt(d,p.pythonVersion,u,r,c):await Ut(d,u,r,c,s);if(await Ft(d,p.installMethod),await Vt(d,p.installMethod),u.succeed("RapidKit environment ready!"),!o.skipGit){u.start("Initializing git repository");try{await execa("git",["init"],{cwd:d}),await execa("git",["add","."],{cwd:d}),await execa("git",["commit","-m","Initial commit: RapidKit environment"],{cwd:d}),u.succeed("Git repository initialized");}catch{u.warn("Could not initialize git repository");}}try{let{registerWorkspace:y}=await import('./workspace-LZZGJRGV.js');await y(d,l);}catch{console.warn(g.gray("Note: Could not register workspace in shared registry"));}if(console.log(g.green(`
|
|
62
74
|
\u2728 RapidKit environment created successfully!
|
|
63
|
-
`)),console.log(
|
|
64
|
-
`)),console.log(
|
|
65
|
-
\u{1F4A1} For more information, check the README.md file.`)),console.log(
|
|
66
|
-
\u{1F4DA} RapidKit commands:`)),console.log(
|
|
67
|
-
`));}
|
|
68
|
-
\u274C Error:`),
|
|
75
|
+
`)),console.log(g.cyan("\u{1F4C2} Location:"),g.white(d)),console.log(g.cyan(`\u{1F680} Get started:
|
|
76
|
+
`)),console.log(g.white(` cd ${l}`)),p.installMethod==="poetry"){let y="source $(poetry env info --path)/bin/activate";try{ve();let{stdout:C}=await execa("poetry",["--version"]),E=C.match(/Poetry.*?(\d+)\.(\d+)/);E&&(parseInt(E[1])>=2?y="source $(poetry env info --path)/bin/activate":y="poetry shell");}catch{}console.log(g.white(` ${y} # Or: poetry run rapidkit`)),console.log(g.white(" rapidkit create # Interactive mode")),console.log(g.white(" cd <project-name> && rapidkit init && rapidkit dev"));}else p.installMethod==="venv"?(console.log(g.white(" source .venv/bin/activate # On Windows: .venv\\Scripts\\activate")),console.log(g.white(" rapidkit create # Interactive mode")),console.log(g.white(" cd <project-name> && rapidkit init && rapidkit dev"))):(console.log(g.white(" rapidkit create # Interactive mode")),console.log(g.white(" cd <project-name> && rapidkit init && rapidkit dev")));console.log(g.white(`
|
|
77
|
+
\u{1F4A1} For more information, check the README.md file.`)),console.log(g.cyan(`
|
|
78
|
+
\u{1F4DA} RapidKit commands:`)),console.log(g.white(" rapidkit create - Create a new project (interactive)")),console.log(g.white(" rapidkit dev - Run development server")),console.log(g.white(" rapidkit add module <name> - Add a module (e.g., settings)")),console.log(g.white(" rapidkit list - List available kits")),console.log(g.white(" rapidkit modules - List available modules")),console.log(g.white(` rapidkit --help - Show all commands
|
|
79
|
+
`));try{let{stdout:y}=await execa("go",["version"],{timeout:3e3}),C=y.match(/go version go(\d+\.\d+(?:\.\d+)?)/),E=C?C[1]:"unknown";console.log(g.gray(`\u{1F439} Go toolchain: Go ${E} detected \u2014 ready for gofiber.standard projects`));}catch{console.log(g.yellow("\u26A0\uFE0F Go toolchain not installed \u2014 needed for gofiber.standard projects")),console.log(g.gray(" Install: https://go.dev/dl/"));}console.log("");}catch(h){u.fail("Failed to create RapidKit environment"),console.error(g.red(`
|
|
80
|
+
\u274C Error:`),h);try{await R.remove(d);}catch{}throw h}}async function Ht(e){let o=[];try{let{stdout:t}=await execa("pyenv",["root"]),r=t.trim();o.push(v.join(r,"versions",`${e}.*`,"bin","python"));let[i,n]=e.split(".");o.push(v.join(r,"versions",`${i}.${n}.*`,"bin","python"));}catch{}o.push(`python${e}`,`python3.${e.split(".")[1]}`,"python3","python"),o.push(`/usr/bin/python${e}`,"/usr/bin/python3",`/usr/local/bin/python${e}`,"/usr/local/bin/python3");for(let t of o)try{let r=t;if(t.includes("*")){if(r=(await execa("sh",["-c",`ls -d ${t} 2>/dev/null | head -1`])).stdout.trim(),!r)continue;r=v.join(r.split("/").slice(0,-1).join("/"),"../bin/python");}let{stdout:i}=await execa(r,["--version"],{timeout:2e3}),n=i.match(/Python (\d+\.\d+)/)?.[1];if(n&&parseFloat(n)>=parseFloat(e))return await execa(r,["-c","import sys; sys.exit(0)"],{timeout:2e3}),r}catch{continue}return null}async function Kt(e,o,t,r,i,n=false){await Zo(t,n),t.start("Finding Python interpreter");let s=await Ht(o);s?(a.debug(`Found working Python: ${s}`),t.succeed("Python found")):t.warn("Could not verify Python path, proceeding with default"),t.start("Initializing Poetry project"),await execa("poetry",["init","--no-interaction","--python",`^${o}`],{cwd:e}),t.succeed("Poetry project initialized");let c=v.join(e,"pyproject.toml"),l=await promises.readFile(c,"utf-8");l.includes("[tool.poetry]")?l=l.replace("[tool.poetry]",`[tool.poetry]
|
|
69
81
|
package-mode = false`):l.includes("[project]")&&(l.includes("[build-system]")?l=l.replace("[build-system]",`
|
|
70
82
|
[tool.poetry]
|
|
71
83
|
package-mode = false
|
|
@@ -74,26 +86,26 @@ package-mode = false
|
|
|
74
86
|
|
|
75
87
|
[tool.poetry]
|
|
76
88
|
package-mode = false
|
|
77
|
-
`),await promises.writeFile(c,l,"utf-8"),t.start("Configuring Poetry");try{if(await execa("poetry",["config","virtualenvs.in-project","true","--local"],{cwd:e}),s)try{await execa("poetry",["env","use",s],{cwd:e}),a.debug(`Poetry configured to use: ${s}`);}catch(d){a.debug(`Could not set Poetry env to ${s}: ${d}`);}t.succeed("Poetry configured");}catch{t.warn("Could not configure Poetry virtualenvs.in-project");}t.start("Creating virtualenv");try{await execa("poetry",["install","--no-root"],{cwd:e,timeout:3e4}),t.succeed("Virtualenv created");}catch(d){a.debug(`Failed to create virtualenv: ${d}`),t.warn("Could not create virtualenv, proceeding with add command");}if(t.start("Installing RapidKit"),
|
|
78
|
-
Error: ${
|
|
89
|
+
`),await promises.writeFile(c,l,"utf-8"),t.start("Configuring Poetry");try{if(await execa("poetry",["config","virtualenvs.in-project","true","--local"],{cwd:e}),s)try{await execa("poetry",["env","use",s],{cwd:e}),a.debug(`Poetry configured to use: ${s}`);}catch(d){a.debug(`Could not set Poetry env to ${s}: ${d}`);}t.succeed("Poetry configured");}catch{t.warn("Could not configure Poetry virtualenvs.in-project");}t.start("Creating virtualenv");try{await execa("poetry",["install","--no-root"],{cwd:e,timeout:3e4}),t.succeed("Virtualenv created");}catch(d){a.debug(`Failed to create virtualenv: ${d}`),t.warn("Could not create virtualenv, proceeding with add command");}if(t.start("Installing RapidKit"),r){let d=Ae(i||{});if(!d)throw new F("Test mode installation",new Error("No local RapidKit path configured. Set RAPIDKIT_DEV_PATH environment variable."));a.debug(`Installing from local path: ${d}`),t.text="Installing RapidKit from local path (test mode)",await execa("poetry",["add",d],{cwd:e});}else {t.text="Installing RapidKit from PyPI";let d=false,p=null;for(let u=1;u<=3;u++)try{await execa("poetry",["add","rapidkit-core"],{cwd:e,timeout:6e4*u}),d=true;break}catch(h){p=h,a.debug(`Poetry add attempt ${u} failed: ${h}`),u<3&&(t.text=`Retrying installation (attempt ${u+1}/3)`,await new Promise(x=>setTimeout(x,2e3)));}if(!d){let u=p?.stderr||p?.message||"Unknown error";throw a.debug(`All Poetry install attempts failed. Last error: ${u}`),u.includes("Could not find")||u.includes("No matching distribution")?new le:new F("Install rapidkit-core with Poetry",new Error(`Failed to install rapidkit-core after 3 attempts.
|
|
90
|
+
Error: ${u}
|
|
79
91
|
|
|
80
92
|
Possible solutions:
|
|
81
93
|
1. Check your internet connection
|
|
82
|
-
2. Try installing manually: cd ${
|
|
83
|
-
3. Use venv method instead: npx rapidkit ${
|
|
94
|
+
2. Try installing manually: cd ${v.basename(e)} && poetry add rapidkit-core
|
|
95
|
+
3. Use venv method instead: npx rapidkit ${v.basename(e)} --install-method=venv`))}}t.succeed("RapidKit installed in project virtualenv");try{let{checkRapidkitCoreAvailable:d}=await import('./pythonRapidkitExec-YIFUZLND.js');if(!await d()&&!r){t.start("Installing RapidKit globally with pipx for CLI access");let u=await je(t,n);try{await pe(u,["install","rapidkit-core"]),t.succeed("RapidKit installed globally");}catch(h){t.warn("Could not install globally (non-fatal, project virtualenv has RapidKit)"),a.debug(`pipx install failed: ${h}`);}}}catch(d){a.debug(`Global install check skipped: ${d}`);}}async function dt(e,o,t,r,i,n=false){t.start(`Checking Python ${o}`);let s=Ne();try{let{stdout:a}=await execa(s,["--version"]),l=a.match(/Python (\d+\.\d+)/)?.[1];if(l&&parseFloat(l)<parseFloat(o))throw new de(o,l);t.succeed(`Python ${l} found`);}catch(a){throw a instanceof de?a:new de(o)}t.start("Creating virtual environment");try{await execa(s,["-m","venv",".venv"],{cwd:e}),t.succeed("Virtual environment created");}catch(a){if(t.fail("Failed to create virtual environment"),(d=>typeof d=="object"&&d!==null&&"stdout"in d&&typeof d.stdout=="string")(a)&&a.stdout.includes("ensurepip is not")){let d=a.stdout.match(/apt install (python[\d.]+-venv)/),p=d?d[1]:"python3-venv";throw new F("Python venv module not available",new Error(`Virtual environment creation failed.
|
|
84
96
|
|
|
85
97
|
On Debian/Ubuntu systems, install the venv package:
|
|
86
|
-
sudo apt install ${
|
|
98
|
+
sudo apt install ${p}
|
|
87
99
|
|
|
88
100
|
Or use Poetry instead (recommended):
|
|
89
|
-
npx rapidkit ${
|
|
101
|
+
npx rapidkit ${v.basename(e)} --yes`))}throw new F("Virtual environment creation",a instanceof Error?a:new Error(String(a)))}t.start("Installing RapidKit");let c=v.join(e,".venv",process.platform==="win32"?"Scripts":"bin",process.platform==="win32"?"python.exe":"python");if(await execa(c,["-m","pip","install","--upgrade","pip"],{cwd:e}),r){let a$1=Ae(i||{});if(!a$1)throw new F("Test mode installation",new Error("No local RapidKit path configured. Set RAPIDKIT_DEV_PATH environment variable."));a.debug(`Installing from local path: ${a$1}`),t.text="Installing RapidKit from local path (test mode)",await execa(c,["-m","pip","install","-e",a$1],{cwd:e});}else {t.text="Installing RapidKit from PyPI";let a$1=false,l=null;for(let d=1;d<=3;d++)try{await execa(c,["-m","pip","install","rapidkit-core"],{cwd:e,timeout:6e4*d}),a$1=true;break}catch(p){l=p,a.debug(`pip install attempt ${d} failed: ${p}`),d<3&&(t.text=`Retrying installation (attempt ${d+1}/3)`,await new Promise(u=>setTimeout(u,2e3)));}if(!a$1){let d=l?.stderr||l?.message||"Unknown error";throw a.debug(`All pip install attempts failed. Last error: ${d}`),d.includes("Could not find")||d.includes("No matching distribution")?new le:new F("Install rapidkit-core with pip",new Error(`Failed to install rapidkit-core after 3 attempts.
|
|
90
102
|
Error: ${d}
|
|
91
103
|
|
|
92
104
|
Possible solutions:
|
|
93
105
|
1. Check your internet connection
|
|
94
|
-
2. Try installing manually: cd ${
|
|
95
|
-
3. Use Poetry instead: npx rapidkit ${
|
|
96
|
-
`,"utf-8");}async function
|
|
106
|
+
2. Try installing manually: cd ${v.basename(e)} && .venv/bin/python -m pip install rapidkit-core
|
|
107
|
+
3. Use Poetry instead: npx rapidkit ${v.basename(e)} --install-method=poetry`))}}t.succeed("RapidKit installed in project virtualenv");try{let{checkRapidkitCoreAvailable:a$1}=await import('./pythonRapidkitExec-YIFUZLND.js');if(!await a$1()&&!r){t.start("Installing RapidKit globally with pipx for CLI access");let d=await je(t,n);try{await pe(d,["install","rapidkit-core"]),t.succeed("RapidKit installed globally");}catch(p){t.warn("Could not install globally (non-fatal, project virtualenv has RapidKit)"),a.debug(`pipx install failed: ${p}`);}}}catch(a$1){a.debug(`Global install check skipped: ${a$1}`);}}async function Ut(e,o,t,r,i=false){let n=await je(o,i);if(o.start("Installing RapidKit globally with pipx"),t){let s=Ae(r||{});if(!s)throw new F("Test mode installation",new Error("No local RapidKit path configured. Set RAPIDKIT_DEV_PATH environment variable."));a.debug(`Installing from local path: ${s}`),o.text="Installing RapidKit from local path (test mode)",await pe(n,["install","-e",s]);}else {o.text="Installing RapidKit from PyPI";try{await pe(n,["install","rapidkit-core"]);}catch{throw new le}}o.succeed("RapidKit installed globally"),await R.outputFile(v.join(e,".rapidkit-global"),`RapidKit installed globally with pipx
|
|
108
|
+
`,"utf-8");}async function te(e,o){let{skipGit:t=false,testMode:r=false,userConfig:i={},yes:n=false,installMethod:s,pythonVersion:c="3.10"}=o||{},a=s||i.defaultInstallMethod||"poetry";await Mt(e,v.basename(e),a),await qt(e),await Lt(e,v.basename(e),a,c);let l=pt("Registering workspace").start();try{a==="poetry"?await Kt(e,c,l,r,i,n):a==="venv"?await dt(e,c,l,r,i):await Ut(e,l,r,i,n),await Ft(e,a),await Vt(e,a),l.succeed("Workspace registered");try{let{registerWorkspace:d}=await import('./workspace-LZZGJRGV.js');await d(e,v.basename(e));}catch{}if(!t){l.start("Initializing git repository");try{await execa("git",["init"],{cwd:e}),await execa("git",["add","."],{cwd:e}),await execa("git",["commit","-m","Initial commit: RapidKit workspace"],{cwd:e}),l.succeed("Git repository initialized");}catch{l.warn("Could not initialize git repository");}}}catch(d){throw l.fail("Failed to register workspace"),d}}async function Vt(e,o){let i=`# RapidKit Workspace
|
|
97
109
|
|
|
98
110
|
This directory contains a RapidKit development environment.
|
|
99
111
|
|
|
@@ -204,7 +216,7 @@ If you encounter issues:
|
|
|
204
216
|
2. Check RapidKit installation: \`rapidkit --version\`
|
|
205
217
|
3. Run diagnostics: \`rapidkit doctor\`
|
|
206
218
|
4. Visit RapidKit documentation or GitHub issues
|
|
207
|
-
`;await promises.writeFile(
|
|
219
|
+
`;await promises.writeFile(v.join(e,"README.md"),i,"utf-8");}async function or(e,o,t){let r=pt("Creating demo workspace").start();try{await R.ensureDir(e),r.succeed("Directory created"),r.start("Setting up demo kit generator");let i=JSON.stringify({name:`${o}-workspace`,version:"1.0.0",private:true,description:"RapidKit demo workspace",scripts:{generate:"node generate-demo.js"}},null,2);await promises.writeFile(v.join(e,"package.json"),i,"utf-8"),await promises.writeFile(v.join(e,"generate-demo.js"),`#!/usr/bin/env node
|
|
208
220
|
/**
|
|
209
221
|
* Demo Kit Generator - Create FastAPI demo projects
|
|
210
222
|
*
|
|
@@ -600,7 +612,7 @@ venv/
|
|
|
600
612
|
}
|
|
601
613
|
|
|
602
614
|
main().catch(console.error);
|
|
603
|
-
`,"utf-8");try{await execa("chmod",["+x",
|
|
615
|
+
`,"utf-8");try{await execa("chmod",["+x",v.join(e,"generate-demo.js")]);}catch{}let s=`# RapidKit Demo Workspace
|
|
604
616
|
|
|
605
617
|
Welcome to your RapidKit demo workspace! This environment lets you generate FastAPI demo projects using bundled RapidKit templates, without needing to install Python RapidKit.
|
|
606
618
|
|
|
@@ -687,7 +699,7 @@ ${o}/
|
|
|
687
699
|
---
|
|
688
700
|
|
|
689
701
|
**Generated with RapidKit** | [GitHub](https://github.com/getrapidkit/rapidkit-npm)
|
|
690
|
-
`;if(await promises.writeFile(
|
|
702
|
+
`;if(await promises.writeFile(v.join(e,"README.md"),s,"utf-8"),r.succeed("Demo workspace setup complete"),!t){r.start("Initializing git repository");try{await execa("git",["init"],{cwd:e}),await R.outputFile(v.join(e,".gitignore"),`# Dependencies
|
|
691
703
|
node_modules/
|
|
692
704
|
|
|
693
705
|
# Generated projects
|
|
@@ -700,19 +712,19 @@ __pycache__/
|
|
|
700
712
|
*.pyc
|
|
701
713
|
.venv/
|
|
702
714
|
.env
|
|
703
|
-
`,"utf-8"),await execa("git",["add","."],{cwd:e}),await execa("git",["commit","-m","Initial commit: Demo workspace"],{cwd:e}),
|
|
715
|
+
`,"utf-8"),await execa("git",["add","."],{cwd:e}),await execa("git",["commit","-m","Initial commit: Demo workspace"],{cwd:e}),r.succeed("Git repository initialized");}catch{r.warn("Could not initialize git repository");}}console.log(g.green(`
|
|
704
716
|
\u2728 Demo workspace created successfully!
|
|
705
|
-
`)),console.log(
|
|
706
|
-
`)),console.log(
|
|
717
|
+
`)),console.log(g.cyan("\u{1F4C2} Location:"),g.white(e)),console.log(g.cyan(`\u{1F680} Get started:
|
|
718
|
+
`)),console.log(g.white(` cd ${o}`)),console.log(g.white(" node generate-demo.js my-api")),console.log(g.white(" cd my-api")),console.log(g.white(" rapidkit init")),console.log(g.white(" rapidkit dev")),console.log(),console.log(g.yellow("\u{1F4A1} Note:"),"This is a demo workspace. For full RapidKit features:"),console.log(g.cyan(" pipx install rapidkit")),console.log();}catch(i){throw r.fail("Failed to create demo workspace"),i}}async function rr(e,o,t,r){console.log(g.cyan(`
|
|
707
719
|
\u{1F50D} Dry-run mode - showing what would be created:
|
|
708
|
-
`)),console.log(
|
|
709
|
-
\u{1F4DD} Files to create:`)),console.log(
|
|
710
|
-
\u{1F3AF} Capabilities:`)),console.log(
|
|
711
|
-
\u2699\uFE0F Configuration:`)),console.log(
|
|
712
|
-
\u{1F4DD} Files to create:`)),console.log(
|
|
713
|
-
\u{1F3AF} Next steps after creation:`)),console.log(
|
|
720
|
+
`)),console.log(g.white("\u{1F4C2} Project path:"),e),console.log(g.white("\u{1F4E6} Project type:"),t?"Demo workspace":"Full RapidKit environment"),t?(console.log(g.white(`
|
|
721
|
+
\u{1F4DD} Files to create:`)),console.log(g.gray(" - package.json")),console.log(g.gray(" - generate-demo.js (project generator)")),console.log(g.gray(" - README.md")),console.log(g.gray(" - .gitignore")),console.log(g.white(`
|
|
722
|
+
\u{1F3AF} Capabilities:`)),console.log(g.gray(" - Generate multiple FastAPI demo projects")),console.log(g.gray(" - No Python RapidKit installation required")),console.log(g.gray(" - Bundled templates included"))):(console.log(g.white(`
|
|
723
|
+
\u2699\uFE0F Configuration:`)),console.log(g.gray(` - Python version: ${r.pythonVersion||"3.10"}`)),console.log(g.gray(` - Install method: ${r.defaultInstallMethod||"poetry"}`)),console.log(g.gray(` - Git initialization: ${r.skipGit?"No":"Yes"}`)),console.log(g.white(`
|
|
724
|
+
\u{1F4DD} Files to create:`)),console.log(g.gray(" - pyproject.toml (Poetry) or .venv/ (venv)")),console.log(g.gray(" - README.md")),console.log(g.gray(" - .gitignore")),console.log(g.white(`
|
|
725
|
+
\u{1F3AF} Next steps after creation:`)),console.log(g.gray(" 1. Install RapidKit Python package")),console.log(g.gray(" 2. Create projects with rapidkit CLI")),console.log(g.gray(" 3. Add modules and customize"))),console.log(g.white(`
|
|
714
726
|
\u{1F4A1} To proceed with actual creation, run without --dry-run flag
|
|
715
|
-
`));}var
|
|
727
|
+
`));}var ar=fileURLToPath(import.meta.url),cr=v.dirname(ar);function dr(e=32){let o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",t=sr.randomBytes(e),r="";for(let i=0;i<e;i++)r+=o[t[i]%o.length];return r}async function Wt(e,o){let t=o.template||"fastapi",r=t==="fastapi",i=r?"FastAPI":"NestJS",n=pt(`Generating ${i} project...`).start();try{let s=v.resolve(cr,".."),c$1=o.kit_name||`${t}.standard`,a;c$1==="fastapi.ddd"?a="fastapi-ddd":c$1.startsWith("fastapi")?a="fastapi-standard":a="nestjs-standard";let l=v.join(s,"templates","kits",a),d=nr.configure(l,{autoescape:false,trimBlocks:true,lstripBlocks:true});d.addFilter("generate_secret",function(y,C=32){return dr(C)});let p={project_name:o.project_name,author:o.author||"RapidKit User",description:o.description||(r?"FastAPI service generated with RapidKit":"NestJS application generated with RapidKit"),app_version:o.app_version||"0.1.0",license:o.license||"MIT",package_manager:o.package_manager||"npm",node_version:o.node_version||"20.0.0",database_type:o.database_type||"postgresql",include_caching:o.include_caching||false,created_at:new Date().toISOString(),rapidkit_version:c()},u;r?u=["src/main.py.j2","src/__init__.py.j2","src/cli.py.j2","src/routing/__init__.py.j2","src/routing/health.py.j2","src/modules/__init__.py.j2","tests/__init__.py.j2","README.md.j2","pyproject.toml.j2","Makefile.j2",".rapidkit/__init__.py.j2",".rapidkit/project.json.j2",".rapidkit/cli.py.j2",".rapidkit/rapidkit.j2",".rapidkit/activate.j2","rapidkit.j2","rapidkit.cmd.j2"]:u=["src/main.ts.j2","src/app.module.ts.j2","src/app.controller.ts.j2","src/app.service.ts.j2","src/config/configuration.ts.j2","src/config/validation.ts.j2","src/config/index.ts.j2","src/modules/index.ts.j2","src/examples/examples.module.ts.j2","src/examples/examples.controller.ts.j2","src/examples/examples.service.ts.j2","src/examples/dto/create-note.dto.ts.j2","test/app.controller.spec.ts.j2","test/examples.controller.spec.ts.j2","test/app.e2e-spec.ts.j2","test/jest-e2e.json.j2","package.json.j2","tsconfig.json.j2","tsconfig.build.json.j2","nest-cli.json.j2","jest.config.ts.j2","eslint.config.cjs.j2",".env.example.j2","docker-compose.yml.j2","Dockerfile.j2","README.md.j2",".rapidkit/project.json.j2",".rapidkit/rapidkit.j2",".rapidkit/rapidkit.cmd.j2",".rapidkit/activate.j2","rapidkit.j2","rapidkit.cmd.j2"];for(let y of u){let C=v.join(l,y);try{await promises.access(C);}catch{continue}let E=await promises.readFile(C,"utf-8"),q;try{q=d.renderString(E,p);}catch(st){throw console.error(`Failed to render template: ${y}`),st}let V=y.replace(/\.j2$/,""),Ie=v.join(e,V);await promises.mkdir(v.dirname(Ie),{recursive:true}),await promises.writeFile(Ie,q),(V.endsWith(".rapidkit/rapidkit")||V.endsWith(".rapidkit/cli.py")||V.endsWith(".rapidkit/activate")||V==="rapidkit")&&await promises.chmod(Ie,493);}if(r){let y=v.join(l,".rapidkit","context.json"),C=v.join(e,".rapidkit","context.json");try{await promises.mkdir(v.join(e,".rapidkit"),{recursive:true}),await promises.copyFile(y,C);}catch{await promises.mkdir(v.join(e,".rapidkit"),{recursive:true});let q=o.engine||"pip";await promises.writeFile(C,JSON.stringify({engine:q,created_by:"rapidkit-npm-fallback"},null,2));}}let h=r?`# Python
|
|
716
728
|
__pycache__/
|
|
717
729
|
*.py[cod]
|
|
718
730
|
*$py.class
|
|
@@ -780,15 +792,15 @@ Thumbs.db
|
|
|
780
792
|
|
|
781
793
|
# Coverage
|
|
782
794
|
coverage/
|
|
783
|
-
`;if(await promises.writeFile(
|
|
784
|
-
${
|
|
785
|
-
${
|
|
786
|
-
${
|
|
787
|
-
${
|
|
788
|
-
`),console.log(
|
|
789
|
-
${
|
|
790
|
-
|
|
791
|
-
${
|
|
795
|
+
`;if(await promises.writeFile(v.join(e,".gitignore"),h),n.succeed(`${i} project generated!`),!o.skipGit){let y=pt("Initializing git repository...").start();try{await execa("git",["init"],{cwd:e}),await execa("git",["add","."],{cwd:e}),await execa("git",["commit","-m",`Initial commit: ${i} project via RapidKit`],{cwd:e}),y.succeed("Git repository initialized");}catch{y.warn("Could not initialize git repository");}}if(!r&&!o.skipInstall){let y=o.package_manager||"npm",C=pt(`Installing dependencies with ${y}...`).start();try{await execa(y,y==="yarn"?["install"]:y==="pnpm"?["install"]:["install"],{cwd:e}),C.succeed("Dependencies installed");}catch{C.warn(`Could not install dependencies. Run '${y} install' manually.`);}}let x=v.basename(e);console.log(`
|
|
796
|
+
${g.yellow("\u26A0\uFE0F Limited offline mode:")} This project was created using basic templates.
|
|
797
|
+
${g.gray("For full kit features, install Python 3.10+ and rapidkit-core:")}
|
|
798
|
+
${g.cyan(" sudo apt install python3 python3-pip python3-venv")}
|
|
799
|
+
${g.cyan(" pip install rapidkit-core")}
|
|
800
|
+
`),console.log(r?`
|
|
801
|
+
${g.green("\u2728 FastAPI project created successfully!")}
|
|
802
|
+
|
|
803
|
+
${g.bold("\u{1F4C2} Project structure:")}
|
|
792
804
|
${e}/
|
|
793
805
|
\u251C\u2500\u2500 .rapidkit/ # RapidKit CLI module
|
|
794
806
|
\u251C\u2500\u2500 src/
|
|
@@ -800,12 +812,12 @@ ${e}/
|
|
|
800
812
|
\u251C\u2500\u2500 pyproject.toml # Poetry configuration
|
|
801
813
|
\u2514\u2500\u2500 README.md
|
|
802
814
|
|
|
803
|
-
${
|
|
804
|
-
${
|
|
805
|
-
${
|
|
806
|
-
${
|
|
815
|
+
${g.bold("\u{1F680} Get started:")}
|
|
816
|
+
${g.cyan(`cd ${x}`)}
|
|
817
|
+
${g.cyan("npx rapidkit init")} ${g.gray("# Install dependencies")}
|
|
818
|
+
${g.cyan("npx rapidkit dev")} ${g.gray("# Start dev server")}
|
|
807
819
|
|
|
808
|
-
${
|
|
820
|
+
${g.bold("\u{1F4DA} Available commands:")}
|
|
809
821
|
npx rapidkit init # Install dependencies (poetry install)
|
|
810
822
|
npx rapidkit dev # Start dev server with hot reload
|
|
811
823
|
npx rapidkit start # Start production server
|
|
@@ -813,12 +825,12 @@ ${u.bold("\u{1F4DA} Available commands:")}
|
|
|
813
825
|
npx rapidkit lint # Lint code
|
|
814
826
|
npx rapidkit format # Format code
|
|
815
827
|
|
|
816
|
-
${
|
|
817
|
-
${
|
|
828
|
+
${g.gray("Alternative: make dev, ./rapidkit dev, poetry run dev")}
|
|
829
|
+
${g.gray("\u{1F4A1} Tip: Install globally (npm i -g rapidkit) to use without npx")}
|
|
818
830
|
`:`
|
|
819
|
-
${
|
|
831
|
+
${g.green("\u2728 NestJS project created successfully!")}
|
|
820
832
|
|
|
821
|
-
${
|
|
833
|
+
${g.bold("\u{1F4C2} Project structure:")}
|
|
822
834
|
${e}/
|
|
823
835
|
\u251C\u2500\u2500 .rapidkit/ # RapidKit CLI module
|
|
824
836
|
\u251C\u2500\u2500 src/
|
|
@@ -830,13 +842,13 @@ ${e}/
|
|
|
830
842
|
\u251C\u2500\u2500 package.json # Dependencies
|
|
831
843
|
\u2514\u2500\u2500 README.md
|
|
832
844
|
|
|
833
|
-
${
|
|
834
|
-
${
|
|
835
|
-
${
|
|
836
|
-
${
|
|
837
|
-
${
|
|
845
|
+
${g.bold("\u{1F680} Get started:")}
|
|
846
|
+
${g.cyan(`cd ${x}`)}
|
|
847
|
+
${g.cyan("npx rapidkit init")} ${g.gray("# Install dependencies")}
|
|
848
|
+
${g.cyan("cp .env.example .env")}
|
|
849
|
+
${g.cyan("npx rapidkit dev")} ${g.gray("# Start dev server")}
|
|
838
850
|
|
|
839
|
-
${
|
|
851
|
+
${g.bold("\u{1F4DA} Available commands:")}
|
|
840
852
|
npx rapidkit init # Install dependencies
|
|
841
853
|
npx rapidkit dev # Start dev server with hot reload
|
|
842
854
|
npx rapidkit start # Start production server
|
|
@@ -845,160 +857,3399 @@ ${u.bold("\u{1F4DA} Available commands:")}
|
|
|
845
857
|
npx rapidkit lint # Lint code
|
|
846
858
|
npx rapidkit format # Format code
|
|
847
859
|
|
|
848
|
-
${
|
|
860
|
+
${g.bold("\u{1F310} API endpoints:")}
|
|
849
861
|
http://localhost:8000/health # Health check
|
|
850
862
|
http://localhost:8000/docs # Swagger docs
|
|
851
863
|
http://localhost:8000/examples/notes # Example API
|
|
852
864
|
|
|
853
|
-
${
|
|
854
|
-
${
|
|
855
|
-
`);}catch(s){throw r.fail(`Failed to generate ${i} project`),s}}async function mt(){let e=process.platform==="win32"?["python","python3"]:["python3","python"];for(let o of e)try{let{stdout:t}=await execa(o,["--version"],{timeout:3e3}),n=t.match(/Python (\d+\.\d+\.\d+)/);if(n){let i=n[1],[r,s]=i.split(".").map(Number);return r<3||r===3&&s<10?{status:"warn",message:`Python ${i} (requires 3.10+)`,details:`${o} found but version is below minimum requirement`}:{status:"ok",message:`Python ${i}`,details:`Using ${o}`}}}catch{continue}return {status:"error",message:"Python not found",details:"Install Python 3.10+ and ensure it's in PATH"}}async function gt(){try{let{stdout:e}=await execa("poetry",["--version"],{timeout:3e3}),o=e.match(/Poetry .*version ([\d.]+)/);return o?{status:"ok",message:`Poetry ${o[1]}`,details:"Available for dependency management"}:{status:"warn",message:"Poetry version unknown"}}catch{return {status:"warn",message:"Poetry not installed",details:"Optional: Install for better dependency management"}}}async function ft(){try{let{stdout:e}=await execa("pipx",["--version"],{timeout:3e3});return {status:"ok",message:`pipx ${e.trim()}`,details:"Available for global tool installation"}}catch{return {status:"warn",message:"pipx not installed",details:"Optional: Install for isolated Python tools"}}}async function ht(){let e=process.env.HOME||process.env.USERPROFILE||"",o=[],t=[{location:"Global (pipx)",path:w.join(e,".local","bin","rapidkit")},{location:"Global (pipx)",path:w.join(e,"AppData","Roaming","Python","Scripts","rapidkit.exe")},{location:"Global (pyenv)",path:w.join(e,".pyenv","shims","rapidkit")},{location:"Global (system)",path:"/usr/local/bin/rapidkit"},{location:"Global (system)",path:"/usr/bin/rapidkit"}],n=[{location:"Workspace (.venv)",path:w.join(process.cwd(),".venv","bin","rapidkit")},{location:"Workspace (.venv)",path:w.join(process.cwd(),".venv","Scripts","rapidkit.exe")}];for(let{location:r,path:s}of [...t,...n])try{if(await k__default.pathExists(s)){let{stdout:c,exitCode:a}=await execa(s,["--version"],{timeout:3e3,reject:false});if(a===0&&(c.includes("RapidKit Version")||c.includes("RapidKit"))){let l=c.match(/v?([\d.]+(?:rc\d+)?(?:a\d+)?(?:b\d+)?)/);l&&o.push({location:r,path:s,version:l[1]});}}}catch{continue}if(o.length>0)return {status:"ok",message:`RapidKit Core ${o[0].version}`,paths:o.map(s=>({location:s.location,path:s.path,version:s.version}))};try{let{stdout:r,exitCode:s}=await execa("rapidkit",["--version"],{timeout:3e3,reject:false});if(s===0&&(r.includes("RapidKit Version")||r.includes("RapidKit"))){let c=r.match(/v?([\d.]+(?:rc\d+)?(?:a\d+)?(?:b\d+)?)/);if(c)return {status:"ok",message:`RapidKit Core ${c[1]}`,details:"Available via PATH"}}}catch{}try{let{stdout:r,exitCode:s}=await execa("poetry",["run","rapidkit","--version"],{timeout:3e3,reject:false});if(s===0&&(r.includes("RapidKit Version")||r.includes("RapidKit"))){let c=r.match(/v?([\d.]+(?:rc\d+)?(?:a\d+)?(?:b\d+)?)/);if(c)return {status:"ok",message:`RapidKit Core ${c[1]}`,details:"Available via Poetry"}}}catch{}let i=process.platform==="win32"?["python","python3"]:["python3","python"];for(let r of i)try{let{stdout:s,exitCode:c}=await execa(r,["-c","import rapidkit_core; print(rapidkit_core.__version__)"],{timeout:3e3,reject:false});if(c===0&&s&&!s.includes("Traceback")&&!s.includes("ModuleNotFoundError")){let a=s.trim();if(a)return {status:"ok",message:`RapidKit Core ${a}`,details:`Available in ${r} environment`}}}catch{continue}return {status:"error",message:"RapidKit Core not installed",details:"Install with: pipx install rapidkit-core"}}async function Te(e,o){let t=w.join(e,"Dockerfile");o.hasDocker=await k__default.pathExists(t);let n=w.join(e,"tests"),i=w.join(e,"test");if(o.hasTests=await k__default.pathExists(n)||await k__default.pathExists(i),o.framework==="NestJS"){let r=w.join(e,".eslintrc.js"),s=w.join(e,".eslintrc.json");o.hasCodeQuality=await k__default.pathExists(r)||await k__default.pathExists(s);}else if(o.framework==="FastAPI"){let r=w.join(e,"ruff.toml"),s=w.join(e,"pyproject.toml");if(await k__default.pathExists(s))try{let c=await k__default.readFile(s,"utf8");o.hasCodeQuality=c.includes("[tool.ruff]")||await k__default.pathExists(r);}catch{o.hasCodeQuality=await k__default.pathExists(r);}}try{if(o.framework==="NestJS"){let{stdout:r}=await execa("npm",["audit","--json"],{cwd:e,reject:false});if(r)try{let c=JSON.parse(r).metadata?.vulnerabilities;c&&(o.vulnerabilities=(c.high||0)+(c.critical||0)+(c.moderate||0));}catch{}}else if(o.framework==="FastAPI"){let r=w.join(e,".venv"),s=process.platform==="win32"?w.join(r,"Scripts","python.exe"):w.join(r,"bin","python");if(await k__default.pathExists(s))try{let{stdout:c}=await execa(s,["-m","pip","list","--format=json"],{timeout:5e3,reject:false});if(c){JSON.parse(c);o.vulnerabilities=0;}}catch{}}}catch{}}async function yo(e){let t={name:w.basename(e),path:e,venvActive:false,depsInstalled:false,coreInstalled:false,issues:[],fixCommands:[]},n=w.join(e,".rapidkit");if(!await k__default.pathExists(n))return t.issues.push("Not a valid RapidKit project (missing .rapidkit directory)"),t;try{let a=w.join(e,"registry.json");if(await k__default.pathExists(a)){let l=await k__default.readJson(a);l.installed_modules&&(t.stats={modules:l.installed_modules.length});}}catch{}try{let a=w.join(n,"project.json");if(await k__default.pathExists(a)){let l=await k__default.readJson(a);l.kit&&(t.kit=l.kit);}}catch{}try{let a=w.join(e,".git");if(await k__default.pathExists(a)){let{stdout:l}=await execa("git",["log","-1","--format=%cr"],{cwd:e,reject:false});l&&(t.lastModified=l.trim());}else {let l=await k__default.stat(e),m=Date.now()-l.mtime.getTime(),p=Math.floor(m/(1e3*60*60*24));t.lastModified=p===0?"today":`${p} day${p>1?"s":""} ago`;}}catch{}let i=w.join(e,"package.json"),r=w.join(e,"pyproject.toml"),s=await k__default.pathExists(i),c=await k__default.pathExists(r);if(s){t.framework="NestJS",t.venvActive=true;let a=w.join(e,"node_modules");if(await k__default.pathExists(a))try{let p=(await k__default.readdir(a)).filter(y=>!y.startsWith(".")&&!y.startsWith("_"));t.depsInstalled=p.length>0;}catch{t.depsInstalled=false;}t.depsInstalled||(t.issues.push("Dependencies not installed (node_modules empty or missing)"),t.fixCommands?.push(`cd ${e} && rapidkit init`)),t.coreInstalled=false;let l=w.join(e,".env");if(t.hasEnvFile=await k__default.pathExists(l),!t.hasEnvFile){let m=w.join(e,".env.example");await k__default.pathExists(m)&&(t.issues.push("Environment file missing (found .env.example)"),t.fixCommands?.push(`cd ${e} && cp .env.example .env`));}let d=w.join(e,"src");if(t.modulesHealthy=true,t.missingModules=[],await k__default.pathExists(d))try{let m=await k__default.readdir(d);t.modulesHealthy=m.length>0;}catch{t.modulesHealthy=false;}return await Te(e,t),t}if(c){t.framework="FastAPI";let a=w.join(e,".venv");if(await k__default.pathExists(a)){t.venvActive=true;let p=process.platform==="win32"?w.join(a,"Scripts","python.exe"):w.join(a,"bin","python");if(await k__default.pathExists(p)){try{let{stdout:y}=await execa(p,["-c","import rapidkit_core; print(rapidkit_core.__version__)"],{timeout:2e3});t.coreInstalled=true,t.coreVersion=y.trim();}catch{t.coreInstalled=false;}try{await execa(p,["-c","import fastapi"],{timeout:2e3}),t.depsInstalled=true;}catch{try{let y=w.join(a,"lib");if(await k__default.pathExists(y)){let v=(await k__default.readdir(y)).find(C=>C.startsWith("python"));if(v){let C=w.join(y,v,"site-packages");if(await k__default.pathExists(C)){let M=(await k__default.readdir(C)).filter(U=>!U.startsWith("_")&&!U.includes("dist-info")&&!["pip","setuptools","wheel","pkg_resources"].includes(U));t.depsInstalled=M.length>0;}}}t.depsInstalled||(t.issues.push("Dependencies not installed"),t.fixCommands?.push(`cd ${e} && rapidkit init`));}catch{t.issues.push("Could not verify dependency installation");}}}else t.issues.push("Virtual environment exists but Python executable not found");}else t.issues.push("Virtual environment not created"),t.fixCommands?.push(`cd ${e} && rapidkit init`);let l=w.join(e,".env");if(t.hasEnvFile=await k__default.pathExists(l),!t.hasEnvFile){let p=w.join(e,".env.example");await k__default.pathExists(p)&&(t.issues.push("Environment file missing (found .env.example)"),t.fixCommands?.push(`cd ${e} && cp .env.example .env`));}let d=w.join(e,"src"),m=w.join(e,"modules");if(t.modulesHealthy=true,t.missingModules=[],await k__default.pathExists(d)){let p=w.join(d,"__init__.py");await k__default.pathExists(p)||(t.modulesHealthy=false,t.missingModules.push("src/__init__.py"));}if(await k__default.pathExists(m))try{let p=await yt(m);for(let y of p){let P=w.join(m,y,"__init__.py");await k__default.pathExists(P)||(t.modulesHealthy=false,t.missingModules.push(`modules/${y}/__init__.py`));}}catch{}return !t.modulesHealthy&&t.missingModules.length>0&&t.issues.push(`Missing module init files: ${t.missingModules.join(", ")}`),await Te(e,t),t}return t.issues.push("Unknown project type (no package.json or pyproject.toml)"),await Te(e,t),t}async function yt(e){try{return (await k__default.readdir(e,{withFileTypes:true})).filter(t=>t.isDirectory()).map(t=>t.name)}catch{try{let o=await k__default.readdir(e),t=[];for(let n of o)try{(await k__default.stat(w.join(e,n))).isDirectory()&&t.push(n);}catch{continue}return t}catch{return []}}}async function Ne(e){let o=w.join(e,".rapidkit");if(!await k__default.pathExists(o))return false;let t=["project.json","context.json","file-hashes.json"];for(let n of t)if(await k__default.pathExists(w.join(o,n)))return true;return false}function wt(e,o){if(o.has(e))return true;let t=e.toLowerCase();return !!(t==="dist"||t.startsWith("dist-")||t.startsWith("dist_")||t==="build"||t.startsWith("build-")||t.startsWith("build_"))}async function wo(e,o,t){let n=new Set,i=[{dir:e,depth:0}];for(;i.length>0;){let r=i.shift();if(!r)break;try{let s=await k__default.readdir(r.dir);for(let c of s){if(wt(c,t))continue;let a=w.join(r.dir,c),l;try{l=await k__default.stat(a);}catch{continue}if(l.isDirectory()){if(await Ne(a)){n.add(a);continue}r.depth<o&&i.push({dir:a,depth:r.depth+1});}}}catch{continue}}return Array.from(n)}async function ko(e){let o=e,t=w.parse(o).root;for(;o!==t;){let n=[w.join(o,".rapidkit-workspace"),w.join(o,".rapidkit","workspace-marker.json"),w.join(o,".rapidkit","config.json")];for(let i of n)if(await k__default.pathExists(i))return o;o=w.dirname(o);}return null}function vo(e,o){let t=0,n=0,i=0;return e.forEach(s=>{s.status==="ok"?t++:s.status==="warn"?n++:s.status==="error"&&i++;}),o.forEach(s=>{s.issues.length===0&&s.venvActive&&s.depsInstalled?t++:s.issues.length>0&&n++;}),{total:t+n+i,passed:t,warnings:n,errors:i}}async function bo(e){let o=w.basename(e);try{let i=w.join(e,".rapidkit-workspace");await k__default.pathExists(i)&&(o=(await k__default.readJSON(i)).name||o);}catch{try{let i=w.join(e,".rapidkit","config.json");o=(await k__default.readJSON(i)).workspace_name||o;}catch{}}let t={workspacePath:e,workspaceName:o,python:await mt(),poetry:await gt(),pipx:await ft(),rapidkitCore:await ht(),projects:[]};try{let i=new Set([".git",".venv","node_modules",".rapidkit","dist","build","coverage","__pycache__"]),r=new Set;await Ne(e)&&r.add(e);let s=async(c,a)=>{if(a<0)return;let l=await yt(c);for(let d of l){if(wt(d,i))continue;let m=w.join(c,d);if(await Ne(m)){r.add(m);continue}a>0&&await s(m,a-1);}};if(await s(e,1),a.debug(`Workspace scan (shallow) found ${r.size} project(s)`),r.size===0){let c=await wo(e,3,i);c.forEach(a=>r.add(a)),a.debug(`Workspace scan (deep fallback) found ${c.length} project(s)`);}r.size>0&&a.debug(`Workspace projects detected: ${Array.from(r).join(", ")}`);for(let c of r){let a=await yo(c);t.projects.push(a);}}catch(i){a.debug(`Failed to scan workspace projects: ${i}`);}let n=[t.python,t.poetry,t.pipx,t.rapidkitCore];if(t.healthScore=vo(n,t.projects),t.rapidkitCore.status==="ok"){let i=t.rapidkitCore.message.match(/([\d.]+(?:rc\d+)?(?:a\d+)?(?:b\d+)?)/);i&&(t.coreVersion=i[1]);}return t}function q(e,o){let t=e.status==="ok"?"\u2705":e.status==="warn"?"\u26A0\uFE0F":"\u274C",n=e.status==="ok"?u.green:e.status==="warn"?u.yellow:u.red;console.log(`${t} ${u.bold(o)}: ${n(e.message)}`),e.paths&&e.paths.length>0?e.paths.forEach(i=>{let r=i.version?u.cyan(` -> ${i.version}`):"";console.log(` ${u.cyan("\u2022")} ${u.gray(i.location)}: ${u.dim(i.path)}${r}`);}):e.details&&console.log(` ${u.gray(e.details)}`);}function xo(e){let o=e.issues.length>0,t=o?"\u26A0\uFE0F":"\u2705",n=o?u.yellow:u.green;if(console.log(`
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
865
|
+
${g.gray("Alternative: npm run start:dev, ./rapidkit dev")}
|
|
866
|
+
${g.gray("\u{1F4A1} Tip: Install globally (npm i -g rapidkit) to use without npx")}
|
|
867
|
+
`);}catch(s){throw n.fail(`Failed to generate ${i} project`),s}}function Bt(e){return e.split(/[-_\s]+/).map(o=>o.charAt(0).toUpperCase()+o.slice(1)).join("")}async function pr(e,o){await promises.mkdir(v.dirname(e),{recursive:true}),await promises.writeFile(e,o,"utf8");}function ur(e){return `package main
|
|
868
|
+
|
|
869
|
+
import (
|
|
870
|
+
"fmt"
|
|
871
|
+
"log/slog"
|
|
872
|
+
"os"
|
|
873
|
+
"os/signal"
|
|
874
|
+
"syscall"
|
|
875
|
+
"time"
|
|
876
|
+
|
|
877
|
+
_ "${e.module_path}/docs"
|
|
878
|
+
"${e.module_path}/internal/config"
|
|
879
|
+
"${e.module_path}/internal/server"
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
// Build-time variables \u2014 injected via -ldflags.
|
|
883
|
+
var (
|
|
884
|
+
version = "dev"
|
|
885
|
+
commit = "none"
|
|
886
|
+
date = "unknown"
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
func main() {
|
|
890
|
+
cfg := config.Load()
|
|
891
|
+
|
|
892
|
+
log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
893
|
+
Level: config.ParseLogLevel(cfg.LogLevel),
|
|
894
|
+
}))
|
|
895
|
+
slog.SetDefault(log)
|
|
896
|
+
|
|
897
|
+
app := server.NewApp(cfg)
|
|
898
|
+
|
|
899
|
+
// Graceful shutdown on SIGINT / SIGTERM
|
|
900
|
+
quit := make(chan os.Signal, 1)
|
|
901
|
+
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
|
902
|
+
|
|
903
|
+
go func() {
|
|
904
|
+
slog.Info("starting", "port", cfg.Port, "version", version, "commit", commit, "date", date, "env", cfg.Env)
|
|
905
|
+
fmt.Printf("\\n\u{1F680} Server \u2192 http://127.0.0.1:%s\\n", cfg.Port)
|
|
906
|
+
fmt.Printf("\u{1F4D6} Docs \u2192 http://127.0.0.1:%s/docs\\n\\n", cfg.Port)
|
|
907
|
+
if err := app.Listen(":" + cfg.Port); err != nil {
|
|
908
|
+
slog.Error("server error", "err", err)
|
|
909
|
+
os.Exit(1)
|
|
910
|
+
}
|
|
911
|
+
}()
|
|
912
|
+
|
|
913
|
+
<-quit
|
|
914
|
+
slog.Info("shutting down\u2026")
|
|
915
|
+
if err := app.ShutdownWithTimeout(5 * time.Second); err != nil {
|
|
916
|
+
slog.Error("graceful shutdown failed", "err", err)
|
|
917
|
+
os.Exit(1)
|
|
918
|
+
}
|
|
919
|
+
slog.Info("server stopped")
|
|
920
|
+
}
|
|
921
|
+
`}function gr(e){return `module ${e.module_path}
|
|
922
|
+
|
|
923
|
+
go ${e.go_version}
|
|
924
|
+
|
|
925
|
+
require (
|
|
926
|
+
github.com/gofiber/fiber/v2 v2.52.5
|
|
927
|
+
github.com/swaggo/fiber-swagger v1.3.0
|
|
928
|
+
github.com/swaggo/swag v1.16.3
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
require (
|
|
932
|
+
github.com/KyleBanks/depth v1.2.1 // indirect
|
|
933
|
+
github.com/andybalholm/brotli v1.1.0 // indirect
|
|
934
|
+
github.com/ghodss/yaml v1.0.0 // indirect
|
|
935
|
+
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
|
936
|
+
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
|
937
|
+
github.com/go-openapi/spec v0.21.0 // indirect
|
|
938
|
+
github.com/go-openapi/swag v0.23.0 // indirect
|
|
939
|
+
github.com/josharian/intern v1.0.0 // indirect
|
|
940
|
+
github.com/klauspost/compress v1.17.6 // indirect
|
|
941
|
+
github.com/mailru/easyjson v0.7.7 // indirect
|
|
942
|
+
github.com/mattn/go-colorable v0.1.13 // indirect
|
|
943
|
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
944
|
+
github.com/mattn/go-runewidth v0.0.15 // indirect
|
|
945
|
+
github.com/rivo/uniseg v0.2.0 // indirect
|
|
946
|
+
github.com/swaggo/files v1.0.1 // indirect
|
|
947
|
+
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
|
948
|
+
github.com/valyala/fasthttp v1.52.0 // indirect
|
|
949
|
+
github.com/valyala/tcplisten v1.0.0 // indirect
|
|
950
|
+
golang.org/x/net v0.25.0 // indirect
|
|
951
|
+
golang.org/x/sys v0.16.0 // indirect
|
|
952
|
+
golang.org/x/tools v0.21.0 // indirect
|
|
953
|
+
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
954
|
+
)
|
|
955
|
+
`}function mr(e){return `package config
|
|
956
|
+
|
|
957
|
+
import (
|
|
958
|
+
"log/slog"
|
|
959
|
+
"os"
|
|
960
|
+
"strings"
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
// Config holds application configuration loaded from environment variables.
|
|
964
|
+
type Config struct {
|
|
965
|
+
Port string
|
|
966
|
+
Env string
|
|
967
|
+
LogLevel string
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Load reads configuration from environment variables with sensible defaults.
|
|
971
|
+
func Load() *Config {
|
|
972
|
+
env := getEnv("APP_ENV", "development")
|
|
973
|
+
return &Config{
|
|
974
|
+
Port: getEnv("PORT", "${e.port}"),
|
|
975
|
+
Env: env,
|
|
976
|
+
LogLevel: getEnv("LOG_LEVEL", defaultLogLevel(env)),
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ParseLogLevel maps a level string to the corresponding slog.Level.
|
|
981
|
+
// Falls back to Info for unrecognised values.
|
|
982
|
+
func ParseLogLevel(s string) slog.Level {
|
|
983
|
+
switch strings.ToLower(s) {
|
|
984
|
+
case "debug":
|
|
985
|
+
return slog.LevelDebug
|
|
986
|
+
case "warn", "warning":
|
|
987
|
+
return slog.LevelWarn
|
|
988
|
+
case "error":
|
|
989
|
+
return slog.LevelError
|
|
990
|
+
default:
|
|
991
|
+
return slog.LevelInfo
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
func defaultLogLevel(env string) string {
|
|
996
|
+
if env == "development" {
|
|
997
|
+
return "debug"
|
|
998
|
+
}
|
|
999
|
+
return "info"
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
func getEnv(key, fallback string) string {
|
|
1003
|
+
if v, ok := os.LookupEnv(key); ok && v != "" {
|
|
1004
|
+
return v
|
|
1005
|
+
}
|
|
1006
|
+
return fallback
|
|
1007
|
+
}
|
|
1008
|
+
`}function fr(e){return `package server
|
|
1009
|
+
|
|
1010
|
+
import (
|
|
1011
|
+
"net/http"
|
|
1012
|
+
"time"
|
|
1013
|
+
|
|
1014
|
+
"github.com/gofiber/fiber/v2"
|
|
1015
|
+
"github.com/gofiber/fiber/v2/middleware/recover"
|
|
1016
|
+
fiberSwagger "github.com/swaggo/fiber-swagger"
|
|
1017
|
+
|
|
1018
|
+
"${e.module_path}/internal/apierr"
|
|
1019
|
+
"${e.module_path}/internal/config"
|
|
1020
|
+
"${e.module_path}/internal/handlers"
|
|
1021
|
+
"${e.module_path}/internal/middleware"
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
// NewApp creates and configures the Fiber application.
|
|
1025
|
+
// Call this from main \u2014 or from tests via server.NewApp(cfg).
|
|
1026
|
+
func NewApp(cfg *config.Config) *fiber.App {
|
|
1027
|
+
app := fiber.New(fiber.Config{
|
|
1028
|
+
AppName: "${e.project_name}",
|
|
1029
|
+
ReadTimeout: 5 * time.Second,
|
|
1030
|
+
WriteTimeout: 10 * time.Second,
|
|
1031
|
+
IdleTimeout: 30 * time.Second,
|
|
1032
|
+
// Override default error handler to always return JSON.
|
|
1033
|
+
// The catch-all middleware returns fiber.ErrNotFound so all 404s
|
|
1034
|
+
// are routed here, keeping error formatting in one place.
|
|
1035
|
+
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
|
1036
|
+
code := fiber.StatusInternalServerError
|
|
1037
|
+
if e, ok := err.(*fiber.Error); ok {
|
|
1038
|
+
code = e.Code
|
|
1039
|
+
}
|
|
1040
|
+
if code == http.StatusNotFound {
|
|
1041
|
+
return apierr.NotFound(c, "route not found")
|
|
1042
|
+
}
|
|
1043
|
+
if code == http.StatusMethodNotAllowed {
|
|
1044
|
+
return apierr.MethodNotAllowed(c)
|
|
1045
|
+
}
|
|
1046
|
+
// Fallback for any unexpected error (e.g. panic-recovered 500).
|
|
1047
|
+
return apierr.InternalError(c, err)
|
|
1048
|
+
},
|
|
1049
|
+
})
|
|
1050
|
+
|
|
1051
|
+
app.Use(recover.New())
|
|
1052
|
+
app.Use(middleware.CORS())
|
|
1053
|
+
app.Use(middleware.RequestID())
|
|
1054
|
+
app.Use(middleware.RateLimit())
|
|
1055
|
+
app.Use(middleware.Logger())
|
|
1056
|
+
|
|
1057
|
+
// Swagger UI \u2014 /docs redirects to /docs/index.html
|
|
1058
|
+
app.Get("/docs", func(c *fiber.Ctx) error { return c.Redirect("/docs/index.html", fiber.StatusFound) })
|
|
1059
|
+
app.Get("/docs/*", fiberSwagger.WrapHandler)
|
|
1060
|
+
|
|
1061
|
+
v1 := app.Group("/api/v1")
|
|
1062
|
+
v1.Get("/health/live", handlers.Liveness)
|
|
1063
|
+
v1.Get("/health/ready", handlers.Readiness)
|
|
1064
|
+
v1.Get("/echo/:name", handlers.EchoParams)
|
|
1065
|
+
|
|
1066
|
+
// 404 catch-all: return fiber.ErrNotFound so it is processed by the
|
|
1067
|
+
// custom ErrorHandler above, keeping all error formatting in one place.
|
|
1068
|
+
app.Use(func(c *fiber.Ctx) error {
|
|
1069
|
+
return fiber.ErrNotFound
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
return app
|
|
1073
|
+
}
|
|
1074
|
+
`}function hr(){return `package handlers
|
|
1075
|
+
|
|
1076
|
+
import (
|
|
1077
|
+
"time"
|
|
1078
|
+
|
|
1079
|
+
"github.com/gofiber/fiber/v2"
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
// Liveness signals the process is alive (Kubernetes livenessProbe).
|
|
1083
|
+
//
|
|
1084
|
+
// @Summary Liveness probe
|
|
1085
|
+
// @Description Returns 200 when the process is alive.
|
|
1086
|
+
// @Tags health
|
|
1087
|
+
// @Produce json
|
|
1088
|
+
// @Success 200 {object} map[string]string
|
|
1089
|
+
// @Router /api/v1/health/live [get]
|
|
1090
|
+
func Liveness(c *fiber.Ctx) error {
|
|
1091
|
+
return c.JSON(fiber.Map{
|
|
1092
|
+
"status": "ok",
|
|
1093
|
+
"time": time.Now().UTC().Format(time.RFC3339),
|
|
1094
|
+
})
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Readiness signals the service can accept traffic (Kubernetes readinessProbe).
|
|
1098
|
+
// Extend this function to check database connectivity, caches, etc.
|
|
1099
|
+
//
|
|
1100
|
+
// @Summary Readiness probe
|
|
1101
|
+
// @Description Returns 200 when the service is ready to accept traffic.
|
|
1102
|
+
// @Tags health
|
|
1103
|
+
// @Produce json
|
|
1104
|
+
// @Success 200 {object} map[string]string
|
|
1105
|
+
// @Router /api/v1/health/ready [get]
|
|
1106
|
+
func Readiness(c *fiber.Ctx) error {
|
|
1107
|
+
return c.JSON(fiber.Map{
|
|
1108
|
+
"status": "ready",
|
|
1109
|
+
"time": time.Now().UTC().Format(time.RFC3339),
|
|
1110
|
+
})
|
|
1111
|
+
}
|
|
1112
|
+
`}function wr(e){return `package handlers_test
|
|
1113
|
+
|
|
1114
|
+
import (
|
|
1115
|
+
"encoding/json"
|
|
1116
|
+
"io"
|
|
1117
|
+
"net/http"
|
|
1118
|
+
"net/http/httptest"
|
|
1119
|
+
"testing"
|
|
1120
|
+
|
|
1121
|
+
"${e.module_path}/internal/config"
|
|
1122
|
+
"${e.module_path}/internal/server"
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
func TestLiveness(t *testing.T) {
|
|
1126
|
+
app := server.NewApp(config.Load())
|
|
1127
|
+
req := httptest.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
|
|
1128
|
+
resp, err := app.Test(req, -1)
|
|
1129
|
+
if err != nil {
|
|
1130
|
+
t.Fatalf("request error: %v", err)
|
|
1131
|
+
}
|
|
1132
|
+
defer resp.Body.Close()
|
|
1133
|
+
|
|
1134
|
+
if resp.StatusCode != http.StatusOK {
|
|
1135
|
+
data, _ := io.ReadAll(resp.Body)
|
|
1136
|
+
t.Fatalf("expected 200, got %d: %s", resp.StatusCode, data)
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
var body map[string]any
|
|
1140
|
+
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
1141
|
+
t.Fatalf("invalid JSON: %v", err)
|
|
1142
|
+
}
|
|
1143
|
+
if body["status"] != "ok" {
|
|
1144
|
+
t.Fatalf("expected ok, got %v", body["status"])
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
func TestReadiness(t *testing.T) {
|
|
1149
|
+
app := server.NewApp(config.Load())
|
|
1150
|
+
req := httptest.NewRequest(http.MethodGet, "/api/v1/health/ready", nil)
|
|
1151
|
+
resp, err := app.Test(req, -1)
|
|
1152
|
+
if err != nil {
|
|
1153
|
+
t.Fatalf("request error: %v", err)
|
|
1154
|
+
}
|
|
1155
|
+
defer resp.Body.Close()
|
|
1156
|
+
|
|
1157
|
+
if resp.StatusCode != http.StatusOK {
|
|
1158
|
+
t.Fatalf("expected 200, got %d: %s", resp.StatusCode, resp.Status)
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
`}function yr(){return `# \u2500\u2500 Build stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1162
|
+
FROM golang:1.24-alpine AS builder
|
|
1163
|
+
|
|
1164
|
+
# Build-time version injection
|
|
1165
|
+
ARG VERSION=dev
|
|
1166
|
+
ARG COMMIT=none
|
|
1167
|
+
ARG DATE=unknown
|
|
1168
|
+
|
|
1169
|
+
WORKDIR /app
|
|
1170
|
+
COPY go.mod go.sum ./
|
|
1171
|
+
RUN go mod download
|
|
1172
|
+
|
|
1173
|
+
COPY . .
|
|
1174
|
+
RUN CGO_ENABLED=0 GOOS=linux go build \\
|
|
1175
|
+
-ldflags="-s -w -X main.version=$\${VERSION} -X main.commit=$\${COMMIT} -X main.date=$\${DATE}" \\
|
|
1176
|
+
-o server ./cmd/server
|
|
1177
|
+
|
|
1178
|
+
# \u2500\u2500 Runtime stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
1179
|
+
# alpine includes busybox wget required for the HEALTHCHECK below.
|
|
1180
|
+
FROM alpine:3.21
|
|
1181
|
+
|
|
1182
|
+
RUN addgroup -S app && adduser -S -G app app
|
|
1183
|
+
COPY --from=builder /app/server /server
|
|
1184
|
+
USER app
|
|
1185
|
+
|
|
1186
|
+
EXPOSE 3000
|
|
1187
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
1188
|
+
CMD wget -qO- http://localhost:3000/api/v1/health/live || exit 1
|
|
1189
|
+
ENTRYPOINT ["/server"]
|
|
1190
|
+
`}function vr(e){return `version: "3.9"
|
|
1191
|
+
|
|
1192
|
+
services:
|
|
1193
|
+
api:
|
|
1194
|
+
build: .
|
|
1195
|
+
container_name: ${e.project_name}
|
|
1196
|
+
ports:
|
|
1197
|
+
- "${e.port}:${e.port}"
|
|
1198
|
+
environment:
|
|
1199
|
+
PORT: "${e.port}"
|
|
1200
|
+
APP_ENV: development
|
|
1201
|
+
LOG_LEVEL: info
|
|
1202
|
+
CORS_ALLOW_ORIGINS: "*"
|
|
1203
|
+
RATE_LIMIT_RPS: "100"
|
|
1204
|
+
restart: unless-stopped
|
|
1205
|
+
`}function br(e){return `.PHONY: dev run build test cover lint fmt tidy docs docker-up docker-down
|
|
1206
|
+
|
|
1207
|
+
# Build-time metadata
|
|
1208
|
+
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
|
1209
|
+
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
|
|
1210
|
+
DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1211
|
+
LDFLAGS = -ldflags "-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)"
|
|
1212
|
+
# Go tool binaries are installed to GOPATH/bin; include it so \`air\` and \`swag\` are found.
|
|
1213
|
+
GOBIN ?= $(shell go env GOPATH)/bin
|
|
1214
|
+
|
|
1215
|
+
# Hot reload \u2014 installs air on first use
|
|
1216
|
+
dev:
|
|
1217
|
+
@test -x "$(GOBIN)/air" || go install github.com/air-verse/air@latest
|
|
1218
|
+
$(GOBIN)/air
|
|
1219
|
+
|
|
1220
|
+
run:
|
|
1221
|
+
go run $(LDFLAGS) ./cmd/server
|
|
1222
|
+
|
|
1223
|
+
build:
|
|
1224
|
+
go build $(LDFLAGS) -o bin/${e.project_name} ./cmd/server
|
|
1225
|
+
|
|
1226
|
+
test:
|
|
1227
|
+
go test ./... -v -race
|
|
1228
|
+
|
|
1229
|
+
cover:
|
|
1230
|
+
go test ./... -race -coverprofile=coverage.out
|
|
1231
|
+
go tool cover -html=coverage.out -o coverage.html
|
|
1232
|
+
@echo "Coverage report: coverage.html"
|
|
1233
|
+
|
|
1234
|
+
# Generate Swagger docs \u2014 installs swag on first use
|
|
1235
|
+
docs:
|
|
1236
|
+
@test -x "$(GOBIN)/swag" || go install github.com/swaggo/swag/cmd/swag@latest
|
|
1237
|
+
$(GOBIN)/swag init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency
|
|
1238
|
+
tidy:
|
|
1239
|
+
go mod tidy
|
|
1240
|
+
|
|
1241
|
+
docker-up:
|
|
1242
|
+
go mod tidy
|
|
1243
|
+
docker compose up --build \\
|
|
1244
|
+
--build-arg VERSION=$(VERSION) \\
|
|
1245
|
+
--build-arg COMMIT=$(COMMIT) \\
|
|
1246
|
+
--build-arg DATE=$(DATE) \\
|
|
1247
|
+
-d
|
|
1248
|
+
|
|
1249
|
+
docker-down:
|
|
1250
|
+
docker compose down
|
|
1251
|
+
`}function kr(e){return `# Application
|
|
1252
|
+
PORT=${e.port}
|
|
1253
|
+
APP_ENV=development
|
|
1254
|
+
LOG_LEVEL=debug
|
|
1255
|
+
|
|
1256
|
+
# CORS \u2014 comma-separated list of allowed origins, or * to allow all
|
|
1257
|
+
CORS_ALLOW_ORIGINS=*
|
|
1258
|
+
|
|
1259
|
+
# Rate limiting \u2014 max requests per IP per second
|
|
1260
|
+
RATE_LIMIT_RPS=100
|
|
1261
|
+
`}function Rr(){return `# Binaries
|
|
1262
|
+
bin/
|
|
1263
|
+
*.exe
|
|
1264
|
+
*.exe~
|
|
1265
|
+
*.dll
|
|
1266
|
+
*.so
|
|
1267
|
+
*.dylib
|
|
1268
|
+
|
|
1269
|
+
# Test binary
|
|
1270
|
+
*.test
|
|
1271
|
+
|
|
1272
|
+
# Output of go coverage tool
|
|
1273
|
+
*.out
|
|
1274
|
+
coverage.html
|
|
1275
|
+
|
|
1276
|
+
# Go workspace
|
|
1277
|
+
go.work
|
|
1278
|
+
go.work.sum
|
|
1279
|
+
|
|
1280
|
+
# Environment
|
|
1281
|
+
.env
|
|
1282
|
+
.env.local
|
|
1283
|
+
|
|
1284
|
+
# Hot reload (air)
|
|
1285
|
+
tmp/
|
|
1286
|
+
|
|
1287
|
+
# Swagger \u2014 generated files (committed stub docs/doc.go; run \`make docs\` to regenerate)
|
|
1288
|
+
docs/swagger.json
|
|
1289
|
+
docs/swagger.yaml
|
|
1290
|
+
docs/docs.go
|
|
1291
|
+
|
|
1292
|
+
# Editor
|
|
1293
|
+
.idea/
|
|
1294
|
+
.vscode/
|
|
1295
|
+
*.swp
|
|
1296
|
+
*.swo
|
|
1297
|
+
|
|
1298
|
+
# OS
|
|
1299
|
+
.DS_Store
|
|
1300
|
+
Thumbs.db
|
|
1301
|
+
`}function xr(e){return `name: CI
|
|
1302
|
+
|
|
1303
|
+
on:
|
|
1304
|
+
push:
|
|
1305
|
+
branches: [main, develop]
|
|
1306
|
+
pull_request:
|
|
1307
|
+
branches: [main]
|
|
1308
|
+
|
|
1309
|
+
jobs:
|
|
1310
|
+
test:
|
|
1311
|
+
name: Test
|
|
1312
|
+
runs-on: ubuntu-latest
|
|
1313
|
+
|
|
1314
|
+
steps:
|
|
1315
|
+
- uses: actions/checkout@v4
|
|
1316
|
+
|
|
1317
|
+
- name: Set up Go
|
|
1318
|
+
uses: actions/setup-go@v5
|
|
1319
|
+
with:
|
|
1320
|
+
go-version: "${e.go_version}"
|
|
1321
|
+
cache: true
|
|
1322
|
+
|
|
1323
|
+
- name: Tidy
|
|
1324
|
+
run: go mod tidy
|
|
1325
|
+
|
|
1326
|
+
- name: Build
|
|
1327
|
+
run: go build ./...
|
|
1328
|
+
|
|
1329
|
+
- name: Test
|
|
1330
|
+
run: go test ./... -race -coverprofile=coverage.out
|
|
1331
|
+
|
|
1332
|
+
- name: Upload coverage
|
|
1333
|
+
uses: actions/upload-artifact@v4
|
|
1334
|
+
with:
|
|
1335
|
+
name: coverage
|
|
1336
|
+
path: coverage.out
|
|
1337
|
+
|
|
1338
|
+
lint:
|
|
1339
|
+
name: Lint
|
|
1340
|
+
runs-on: ubuntu-latest
|
|
1341
|
+
|
|
1342
|
+
steps:
|
|
1343
|
+
- uses: actions/checkout@v4
|
|
1344
|
+
|
|
1345
|
+
- name: Set up Go
|
|
1346
|
+
uses: actions/setup-go@v5
|
|
1347
|
+
with:
|
|
1348
|
+
go-version: "${e.go_version}"
|
|
1349
|
+
cache: true
|
|
1350
|
+
|
|
1351
|
+
- name: golangci-lint
|
|
1352
|
+
uses: golangci/golangci-lint-action@v6
|
|
1353
|
+
with:
|
|
1354
|
+
version: latest
|
|
1355
|
+
`}function Cr(e){return `# ${Bt(e.project_name)}
|
|
1356
|
+
|
|
1357
|
+
> ${e.description}
|
|
1358
|
+
|
|
1359
|
+
Built with [Go](https://go.dev/) + [Fiber v2](https://gofiber.io/) \xB7 Scaffolded by [RapidKit](https://getrapidkit.com)
|
|
1360
|
+
|
|
1361
|
+
## Quick start
|
|
1362
|
+
|
|
1363
|
+
\`\`\`bash
|
|
1364
|
+
# Run locally (hot reload)
|
|
1365
|
+
make dev
|
|
1366
|
+
|
|
1367
|
+
# Run tests
|
|
1368
|
+
make test
|
|
1369
|
+
|
|
1370
|
+
# Build binary
|
|
1371
|
+
make build
|
|
1372
|
+
|
|
1373
|
+
# Generate / refresh Swagger docs
|
|
1374
|
+
make docs
|
|
1375
|
+
|
|
1376
|
+
# Docker
|
|
1377
|
+
make docker-up
|
|
1378
|
+
\`\`\`
|
|
1379
|
+
|
|
1380
|
+
## Swagger / OpenAPI
|
|
1381
|
+
|
|
1382
|
+
After running \`make docs\`, the interactive UI is available at:
|
|
1383
|
+
|
|
1384
|
+
\`\`\`
|
|
1385
|
+
http://localhost:${e.port}/docs
|
|
1386
|
+
\`\`\`
|
|
1387
|
+
|
|
1388
|
+
The raw OpenAPI spec is served at \`/docs/doc.json\`.
|
|
1389
|
+
|
|
1390
|
+
## Endpoints
|
|
1391
|
+
|
|
1392
|
+
| Method | Path | Description |
|
|
1393
|
+
|--------|------|-------------|
|
|
1394
|
+
| GET | /api/v1/health/live | Kubernetes livenessProbe |
|
|
1395
|
+
| GET | /api/v1/health/ready | Kubernetes readinessProbe |
|
|
1396
|
+
| GET | /api/v1/echo/:name | Example handler \u2014 remove in production |
|
|
1397
|
+
| GET | /docs/* | Swagger UI (OpenAPI docs) |
|
|
1398
|
+
|
|
1399
|
+
## Configuration
|
|
1400
|
+
|
|
1401
|
+
All configuration is done through environment variables (see \`.env.example\`):
|
|
1402
|
+
|
|
1403
|
+
| Variable | Default | Description |
|
|
1404
|
+
|----------|---------|-------------|
|
|
1405
|
+
| \`PORT\` | \`${e.port}\` | HTTP listen port |
|
|
1406
|
+
| \`APP_ENV\` | \`development\` | Application environment |
|
|
1407
|
+
| \`LOG_LEVEL\` | \`debug\` / \`info\` | \`debug\` \\| \`info\` \\| \`warn\` \\| \`error\` |
|
|
1408
|
+
| \`CORS_ALLOW_ORIGINS\` | \`*\` | Comma-separated list of allowed origins, or \`*\` |
|
|
1409
|
+
| \`RATE_LIMIT_RPS\` | \`100\` | Max requests per IP per second |
|
|
1410
|
+
|
|
1411
|
+
## Project structure
|
|
1412
|
+
|
|
1413
|
+
\`\`\`
|
|
1414
|
+
${e.project_name}/
|
|
1415
|
+
\u251C\u2500\u2500 cmd/
|
|
1416
|
+
\u2502 \u2514\u2500\u2500 server/
|
|
1417
|
+
\u2502 \u2514\u2500\u2500 main.go # Graceful shutdown + version ldflags
|
|
1418
|
+
\u251C\u2500\u2500 docs/ # Swagger generated files (\`make docs\`)
|
|
1419
|
+
\u2502 \u2514\u2500\u2500 doc.go # Package-level OpenAPI annotations
|
|
1420
|
+
\u251C\u2500\u2500 internal/
|
|
1421
|
+
\u2502 \u251C\u2500\u2500 apierr/ # Consistent JSON error envelope
|
|
1422
|
+
\u2502 \u2502 \u251C\u2500\u2500 apierr.go
|
|
1423
|
+
\u2502 \u2502 \u2514\u2500\u2500 apierr_test.go
|
|
1424
|
+
\u2502 \u251C\u2500\u2500 config/ # 12-factor configuration
|
|
1425
|
+
\u2502 \u2502 \u251C\u2500\u2500 config.go
|
|
1426
|
+
\u2502 \u2502 \u2514\u2500\u2500 config_test.go
|
|
1427
|
+
\u2502 \u251C\u2500\u2500 handlers/ # HTTP handlers + tests
|
|
1428
|
+
\u2502 \u2502 \u251C\u2500\u2500 health.go
|
|
1429
|
+
\u2502 \u2502 \u251C\u2500\u2500 health_test.go
|
|
1430
|
+
\u2502 \u2502 \u251C\u2500\u2500 example.go # EchoParams \u2014 replace with your own handlers
|
|
1431
|
+
\u2502 \u2502 \u2514\u2500\u2500 example_test.go
|
|
1432
|
+
\u2502 \u251C\u2500\u2500 middleware/
|
|
1433
|
+
\u2502 \u2502 \u251C\u2500\u2500 requestid.go # X-Request-ID + structured logger
|
|
1434
|
+
\u2502 \u2502 \u251C\u2500\u2500 requestid_test.go
|
|
1435
|
+
\u2502 \u2502 \u251C\u2500\u2500 cors.go # CORS (CORS_ALLOW_ORIGINS)
|
|
1436
|
+
\u2502 \u2502 \u251C\u2500\u2500 cors_test.go
|
|
1437
|
+
\u2502 \u2502 \u251C\u2500\u2500 ratelimit.go # Per-IP limiter (RATE_LIMIT_RPS)
|
|
1438
|
+
\u2502 \u2502 \u2514\u2500\u2500 ratelimit_test.go
|
|
1439
|
+
\u2502 \u2514\u2500\u2500 server/
|
|
1440
|
+
\u2502 \u251C\u2500\u2500 server.go
|
|
1441
|
+
\u2502 \u2514\u2500\u2500 server_test.go
|
|
1442
|
+
\u251C\u2500\u2500 .air.toml # Hot reload
|
|
1443
|
+
\u251C\u2500\u2500 .github/workflows/ci.yml # CI: test + lint
|
|
1444
|
+
\u251C\u2500\u2500 .golangci.yml
|
|
1445
|
+
\u251C\u2500\u2500 Dockerfile # Multi-stage, alpine HEALTHCHECK
|
|
1446
|
+
\u251C\u2500\u2500 docker-compose.yml
|
|
1447
|
+
\u251C\u2500\u2500 Makefile
|
|
1448
|
+
\u2514\u2500\u2500 README.md
|
|
1449
|
+
\`\`\`
|
|
1450
|
+
|
|
1451
|
+
## Available commands
|
|
1452
|
+
|
|
1453
|
+
| Command | Description |
|
|
1454
|
+
|---------|-------------|
|
|
1455
|
+
| \`make dev\` | Hot reload via [air](https://github.com/air-verse/air) |
|
|
1456
|
+
| \`make run\` | Run without hot reload |
|
|
1457
|
+
| \`make build\` | Binary with version ldflags |
|
|
1458
|
+
| \`make test\` | Run tests with race detector |
|
|
1459
|
+
| \`make cover\` | HTML coverage report |
|
|
1460
|
+
| \`make docs\` | Re-generate Swagger JSON (needs \`swag\`) |
|
|
1461
|
+
| \`make lint\` | golangci-lint |
|
|
1462
|
+
| \`make fmt\` | gofmt |
|
|
1463
|
+
| \`make tidy\` | go mod tidy |
|
|
1464
|
+
| \`make docker-up\` | Build & run via Docker Compose |
|
|
1465
|
+
| \`make docker-down\` | Stop |
|
|
1466
|
+
|
|
1467
|
+
## License
|
|
1468
|
+
|
|
1469
|
+
${e.app_version} \xB7 ${e.author}
|
|
1470
|
+
`}function Sr(){return `package middleware
|
|
1471
|
+
|
|
1472
|
+
import (
|
|
1473
|
+
"crypto/rand"
|
|
1474
|
+
"encoding/hex"
|
|
1475
|
+
"log/slog"
|
|
1476
|
+
"time"
|
|
1477
|
+
|
|
1478
|
+
"github.com/gofiber/fiber/v2"
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
const headerRequestID = "X-Request-ID"
|
|
1482
|
+
|
|
1483
|
+
// RequestID injects a unique identifier into every request.
|
|
1484
|
+
// If the caller sends an X-Request-ID header it is reused; otherwise a new one
|
|
1485
|
+
// is generated and written back in the response.
|
|
1486
|
+
func RequestID() fiber.Handler {
|
|
1487
|
+
return func(c *fiber.Ctx) error {
|
|
1488
|
+
id := c.Get(headerRequestID)
|
|
1489
|
+
if id == "" {
|
|
1490
|
+
id = newID()
|
|
1491
|
+
}
|
|
1492
|
+
c.Set(headerRequestID, id)
|
|
1493
|
+
c.Locals("request_id", id)
|
|
1494
|
+
return c.Next()
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Logger emits a structured JSON log line after each request.
|
|
1499
|
+
func Logger() fiber.Handler {
|
|
1500
|
+
return func(c *fiber.Ctx) error {
|
|
1501
|
+
start := time.Now()
|
|
1502
|
+
err := c.Next()
|
|
1503
|
+
slog.Info("http",
|
|
1504
|
+
"method", c.Method(),
|
|
1505
|
+
"path", c.Path(),
|
|
1506
|
+
"status", c.Response().StatusCode(),
|
|
1507
|
+
"bytes", c.Response().Header.ContentLength(),
|
|
1508
|
+
"latency_ms", time.Since(start).Milliseconds(),
|
|
1509
|
+
"ip", c.IP(),
|
|
1510
|
+
"request_id", c.Locals("request_id"),
|
|
1511
|
+
)
|
|
1512
|
+
return err
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
func newID() string {
|
|
1517
|
+
b := make([]byte, 8)
|
|
1518
|
+
if _, err := rand.Read(b); err != nil {
|
|
1519
|
+
return "unknown"
|
|
1520
|
+
}
|
|
1521
|
+
return hex.EncodeToString(b)
|
|
1522
|
+
}
|
|
1523
|
+
`}function _r(e){return `package middleware_test
|
|
1524
|
+
|
|
1525
|
+
import (
|
|
1526
|
+
"net/http"
|
|
1527
|
+
"net/http/httptest"
|
|
1528
|
+
"testing"
|
|
1529
|
+
|
|
1530
|
+
"github.com/gofiber/fiber/v2"
|
|
1531
|
+
|
|
1532
|
+
"${e.module_path}/internal/middleware"
|
|
1533
|
+
)
|
|
1534
|
+
|
|
1535
|
+
func newTestApp() *fiber.App {
|
|
1536
|
+
app := fiber.New()
|
|
1537
|
+
app.Use(middleware.RequestID())
|
|
1538
|
+
app.Use(middleware.Logger())
|
|
1539
|
+
app.Get("/ping", func(c *fiber.Ctx) error {
|
|
1540
|
+
return c.SendString("pong")
|
|
1541
|
+
})
|
|
1542
|
+
return app
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
func TestRequestID_IsGenerated(t *testing.T) {
|
|
1546
|
+
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
1547
|
+
resp, err := newTestApp().Test(req, -1)
|
|
1548
|
+
if err != nil {
|
|
1549
|
+
t.Fatalf("request error: %v", err)
|
|
1550
|
+
}
|
|
1551
|
+
defer resp.Body.Close()
|
|
1552
|
+
|
|
1553
|
+
id := resp.Header.Get("X-Request-ID")
|
|
1554
|
+
if id == "" {
|
|
1555
|
+
t.Fatal("expected X-Request-ID header to be set")
|
|
1556
|
+
}
|
|
1557
|
+
if len(id) != 16 { // 8 random bytes \u2192 16 hex chars
|
|
1558
|
+
t.Fatalf("unexpected request ID length %d, want 16", len(id))
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
func TestRequestID_IsReused(t *testing.T) {
|
|
1563
|
+
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
1564
|
+
req.Header.Set("X-Request-ID", "my-trace-id")
|
|
1565
|
+
resp, err := newTestApp().Test(req, -1)
|
|
1566
|
+
if err != nil {
|
|
1567
|
+
t.Fatalf("request error: %v", err)
|
|
1568
|
+
}
|
|
1569
|
+
defer resp.Body.Close()
|
|
1570
|
+
|
|
1571
|
+
id := resp.Header.Get("X-Request-ID")
|
|
1572
|
+
if id != "my-trace-id" {
|
|
1573
|
+
t.Fatalf("expected X-Request-ID to be reused, got %q", id)
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
`}function Pr(){return `// Package apierr provides a consistent JSON error envelope for all API responses.
|
|
1577
|
+
//
|
|
1578
|
+
// Every error response looks like:
|
|
1579
|
+
//
|
|
1580
|
+
// {"error": "user not found", "code": "NOT_FOUND", "request_id": "a1b2c3d4..."}
|
|
1581
|
+
package apierr
|
|
1582
|
+
|
|
1583
|
+
import (
|
|
1584
|
+
"net/http"
|
|
1585
|
+
|
|
1586
|
+
"github.com/gofiber/fiber/v2"
|
|
1587
|
+
)
|
|
1588
|
+
|
|
1589
|
+
// Response is the standard error envelope returned by all API endpoints.
|
|
1590
|
+
type Response struct {
|
|
1591
|
+
Error string \`json:"error"\`
|
|
1592
|
+
Code string \`json:"code"\`
|
|
1593
|
+
RequestID string \`json:"request_id,omitempty"\`
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
func reply(c *fiber.Ctx, status int, msg, code string) error {
|
|
1597
|
+
rid, _ := c.Locals("request_id").(string)
|
|
1598
|
+
return c.Status(status).JSON(Response{
|
|
1599
|
+
Error: msg,
|
|
1600
|
+
Code: code,
|
|
1601
|
+
RequestID: rid,
|
|
1602
|
+
})
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// BadRequest responds with 400 and code "BAD_REQUEST".
|
|
1606
|
+
func BadRequest(c *fiber.Ctx, msg string) error {
|
|
1607
|
+
return reply(c, http.StatusBadRequest, msg, "BAD_REQUEST")
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// NotFound responds with 404 and code "NOT_FOUND".
|
|
1611
|
+
func NotFound(c *fiber.Ctx, msg string) error {
|
|
1612
|
+
return reply(c, http.StatusNotFound, msg, "NOT_FOUND")
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// Unauthorized responds with 401 and code "UNAUTHORIZED".
|
|
1616
|
+
func Unauthorized(c *fiber.Ctx) error {
|
|
1617
|
+
return reply(c, http.StatusUnauthorized, "authentication required", "UNAUTHORIZED")
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Forbidden responds with 403 and code "FORBIDDEN".
|
|
1621
|
+
func Forbidden(c *fiber.Ctx) error {
|
|
1622
|
+
return reply(c, http.StatusForbidden, "access denied", "FORBIDDEN")
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// MethodNotAllowed responds with 405 and code "METHOD_NOT_ALLOWED".
|
|
1626
|
+
func MethodNotAllowed(c *fiber.Ctx) error {
|
|
1627
|
+
return reply(c, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// InternalError responds with 500 and code "INTERNAL_ERROR".
|
|
1631
|
+
// The original error is intentionally not exposed to the client.
|
|
1632
|
+
func InternalError(c *fiber.Ctx, _ error) error {
|
|
1633
|
+
return reply(c, http.StatusInternalServerError, "an internal error occurred", "INTERNAL_ERROR")
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// TooManyRequests responds with 429 and code "TOO_MANY_REQUESTS".
|
|
1637
|
+
func TooManyRequests(c *fiber.Ctx, msg string) error {
|
|
1638
|
+
return reply(c, http.StatusTooManyRequests, msg, "TOO_MANY_REQUESTS")
|
|
1639
|
+
}
|
|
1640
|
+
`}function Ir(e){return `package apierr_test
|
|
1641
|
+
|
|
1642
|
+
import (
|
|
1643
|
+
"encoding/json"
|
|
1644
|
+
"io"
|
|
1645
|
+
"net/http"
|
|
1646
|
+
"net/http/httptest"
|
|
1647
|
+
"testing"
|
|
1648
|
+
|
|
1649
|
+
"github.com/gofiber/fiber/v2"
|
|
1650
|
+
|
|
1651
|
+
"${e.module_path}/internal/apierr"
|
|
1652
|
+
)
|
|
1653
|
+
|
|
1654
|
+
func makeApp(fn func(*fiber.Ctx) error) *fiber.App {
|
|
1655
|
+
app := fiber.New()
|
|
1656
|
+
app.Get("/test", fn)
|
|
1657
|
+
return app
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
func readJSON(t *testing.T, r io.Reader) apierr.Response {
|
|
1661
|
+
t.Helper()
|
|
1662
|
+
var out apierr.Response
|
|
1663
|
+
if err := json.NewDecoder(r).Decode(&out); err != nil {
|
|
1664
|
+
t.Fatalf("invalid JSON: %v", err)
|
|
1665
|
+
}
|
|
1666
|
+
return out
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
func TestBadRequest(t *testing.T) {
|
|
1670
|
+
app := makeApp(func(c *fiber.Ctx) error { return apierr.BadRequest(c, "invalid email") })
|
|
1671
|
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
1672
|
+
resp, _ := app.Test(req, -1)
|
|
1673
|
+
defer resp.Body.Close()
|
|
1674
|
+
|
|
1675
|
+
if resp.StatusCode != http.StatusBadRequest {
|
|
1676
|
+
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
1677
|
+
}
|
|
1678
|
+
body := readJSON(t, resp.Body)
|
|
1679
|
+
if body.Code != "BAD_REQUEST" {
|
|
1680
|
+
t.Fatalf("expected BAD_REQUEST, got %q", body.Code)
|
|
1681
|
+
}
|
|
1682
|
+
if body.Error != "invalid email" {
|
|
1683
|
+
t.Fatalf("unexpected error message: %q", body.Error)
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
func TestNotFound(t *testing.T) {
|
|
1688
|
+
app := makeApp(func(c *fiber.Ctx) error { return apierr.NotFound(c, "user not found") })
|
|
1689
|
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
1690
|
+
resp, _ := app.Test(req, -1)
|
|
1691
|
+
defer resp.Body.Close()
|
|
1692
|
+
|
|
1693
|
+
if resp.StatusCode != http.StatusNotFound {
|
|
1694
|
+
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
|
1695
|
+
}
|
|
1696
|
+
body := readJSON(t, resp.Body)
|
|
1697
|
+
if body.Code != "NOT_FOUND" {
|
|
1698
|
+
t.Fatalf("expected NOT_FOUND, got %q", body.Code)
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
func TestUnauthorized(t *testing.T) {
|
|
1703
|
+
app := makeApp(func(c *fiber.Ctx) error { return apierr.Unauthorized(c) })
|
|
1704
|
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
1705
|
+
resp, _ := app.Test(req, -1)
|
|
1706
|
+
defer resp.Body.Close()
|
|
1707
|
+
|
|
1708
|
+
if resp.StatusCode != http.StatusUnauthorized {
|
|
1709
|
+
t.Fatalf("expected 401, got %d", resp.StatusCode)
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
func TestForbidden(t *testing.T) {
|
|
1714
|
+
app := makeApp(func(c *fiber.Ctx) error { return apierr.Forbidden(c) })
|
|
1715
|
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
1716
|
+
resp, _ := app.Test(req, -1)
|
|
1717
|
+
defer resp.Body.Close()
|
|
1718
|
+
|
|
1719
|
+
if resp.StatusCode != http.StatusForbidden {
|
|
1720
|
+
t.Fatalf("expected 403, got %d", resp.StatusCode)
|
|
1721
|
+
}
|
|
1722
|
+
body := readJSON(t, resp.Body)
|
|
1723
|
+
if body.Code != "FORBIDDEN" {
|
|
1724
|
+
t.Fatalf("expected FORBIDDEN, got %q", body.Code)
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
func TestMethodNotAllowed(t *testing.T) {
|
|
1729
|
+
app := makeApp(func(c *fiber.Ctx) error { return apierr.MethodNotAllowed(c) })
|
|
1730
|
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
1731
|
+
resp, _ := app.Test(req, -1)
|
|
1732
|
+
defer resp.Body.Close()
|
|
1733
|
+
|
|
1734
|
+
if resp.StatusCode != http.StatusMethodNotAllowed {
|
|
1735
|
+
t.Fatalf("expected 405, got %d", resp.StatusCode)
|
|
1736
|
+
}
|
|
1737
|
+
body := readJSON(t, resp.Body)
|
|
1738
|
+
if body.Code != "METHOD_NOT_ALLOWED" {
|
|
1739
|
+
t.Fatalf("expected METHOD_NOT_ALLOWED, got %q", body.Code)
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
func TestInternalError(t *testing.T) {
|
|
1744
|
+
app := makeApp(func(c *fiber.Ctx) error { return apierr.InternalError(c, nil) })
|
|
1745
|
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
1746
|
+
resp, _ := app.Test(req, -1)
|
|
1747
|
+
defer resp.Body.Close()
|
|
1748
|
+
|
|
1749
|
+
if resp.StatusCode != http.StatusInternalServerError {
|
|
1750
|
+
t.Fatalf("expected 500, got %d", resp.StatusCode)
|
|
1751
|
+
}
|
|
1752
|
+
body := readJSON(t, resp.Body)
|
|
1753
|
+
if body.Code != "INTERNAL_ERROR" {
|
|
1754
|
+
t.Fatalf("expected INTERNAL_ERROR, got %q", body.Code)
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
func TestTooManyRequests(t *testing.T) {
|
|
1759
|
+
app := makeApp(func(c *fiber.Ctx) error { return apierr.TooManyRequests(c, "slow down") })
|
|
1760
|
+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
1761
|
+
resp, _ := app.Test(req, -1)
|
|
1762
|
+
defer resp.Body.Close()
|
|
1763
|
+
|
|
1764
|
+
if resp.StatusCode != http.StatusTooManyRequests {
|
|
1765
|
+
t.Fatalf("expected 429, got %d", resp.StatusCode)
|
|
1766
|
+
}
|
|
1767
|
+
body := readJSON(t, resp.Body)
|
|
1768
|
+
if body.Code != "TOO_MANY_REQUESTS" {
|
|
1769
|
+
t.Fatalf("expected TOO_MANY_REQUESTS, got %q", body.Code)
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
`}function Er(e){return `// Package docs provides the swaggo-generated OpenAPI specification.
|
|
1773
|
+
//
|
|
1774
|
+
// Run \`make docs\` to regenerate after changing handler annotations.
|
|
1775
|
+
//
|
|
1776
|
+
// @title ${Bt(e.project_name)} API
|
|
1777
|
+
// @version ${e.app_version}
|
|
1778
|
+
// @description ${e.description}
|
|
1779
|
+
// @host localhost:${e.port}
|
|
1780
|
+
// @BasePath /
|
|
1781
|
+
// @schemes http https
|
|
1782
|
+
//
|
|
1783
|
+
// @contact.name ${e.author}
|
|
1784
|
+
// @license.name MIT
|
|
1785
|
+
package docs
|
|
1786
|
+
`}function Tr(e){return `package handlers
|
|
1787
|
+
|
|
1788
|
+
import (
|
|
1789
|
+
"net/http"
|
|
1790
|
+
|
|
1791
|
+
"github.com/gofiber/fiber/v2"
|
|
1792
|
+
|
|
1793
|
+
"${e.module_path}/internal/apierr"
|
|
1794
|
+
)
|
|
1795
|
+
|
|
1796
|
+
// EchoResponse is the JSON body returned by EchoParams.
|
|
1797
|
+
type EchoResponse struct {
|
|
1798
|
+
Name string \`json:"name"\`
|
|
1799
|
+
RequestID string \`json:"request_id"\`
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// EchoParams is an example handler demonstrating how to:
|
|
1803
|
+
// - read URL path parameters
|
|
1804
|
+
// - use apierr for consistent JSON error responses
|
|
1805
|
+
// - access the request ID injected by RequestID middleware
|
|
1806
|
+
//
|
|
1807
|
+
// Replace or remove this file once you add your own business logic.
|
|
1808
|
+
//
|
|
1809
|
+
// @Summary Echo path parameter
|
|
1810
|
+
// @Description Returns the :name path parameter together with the request ID.
|
|
1811
|
+
// @Tags example
|
|
1812
|
+
// @Produce json
|
|
1813
|
+
// @Param name path string true "Name to echo"
|
|
1814
|
+
// @Success 200 {object} handlers.EchoResponse
|
|
1815
|
+
// @Failure 400 {object} apierr.Response
|
|
1816
|
+
// @Router /api/v1/echo/{name} [get]
|
|
1817
|
+
func EchoParams(c *fiber.Ctx) error {
|
|
1818
|
+
name := c.Params("name")
|
|
1819
|
+
if name == "" {
|
|
1820
|
+
return apierr.BadRequest(c, "name parameter is required")
|
|
1821
|
+
}
|
|
1822
|
+
rid, _ := c.Locals("request_id").(string)
|
|
1823
|
+
return c.Status(http.StatusOK).JSON(EchoResponse{
|
|
1824
|
+
Name: name,
|
|
1825
|
+
RequestID: rid,
|
|
1826
|
+
})
|
|
1827
|
+
}
|
|
1828
|
+
`}function Ar(e){return `package handlers_test
|
|
1829
|
+
|
|
1830
|
+
import (
|
|
1831
|
+
"encoding/json"
|
|
1832
|
+
"net/http"
|
|
1833
|
+
"net/http/httptest"
|
|
1834
|
+
"testing"
|
|
1835
|
+
|
|
1836
|
+
"github.com/gofiber/fiber/v2"
|
|
1837
|
+
|
|
1838
|
+
"${e.module_path}/internal/handlers"
|
|
1839
|
+
"${e.module_path}/internal/middleware"
|
|
1840
|
+
)
|
|
1841
|
+
|
|
1842
|
+
func newEchoApp() *fiber.App {
|
|
1843
|
+
app := fiber.New()
|
|
1844
|
+
app.Use(middleware.RequestID())
|
|
1845
|
+
app.Get("/echo/:name", handlers.EchoParams)
|
|
1846
|
+
return app
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
func TestEchoParams_Success(t *testing.T) {
|
|
1850
|
+
req := httptest.NewRequest(http.MethodGet, "/echo/alice", nil)
|
|
1851
|
+
resp, err := newEchoApp().Test(req, -1)
|
|
1852
|
+
if err != nil {
|
|
1853
|
+
t.Fatalf("request error: %v", err)
|
|
1854
|
+
}
|
|
1855
|
+
defer resp.Body.Close()
|
|
1856
|
+
|
|
1857
|
+
if resp.StatusCode != http.StatusOK {
|
|
1858
|
+
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
var body map[string]any
|
|
1862
|
+
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
1863
|
+
t.Fatalf("invalid JSON: %v", err)
|
|
1864
|
+
}
|
|
1865
|
+
if body["name"] != "alice" {
|
|
1866
|
+
t.Fatalf("expected name=alice, got %v", body["name"])
|
|
1867
|
+
}
|
|
1868
|
+
if body["request_id"] == nil || body["request_id"] == "" {
|
|
1869
|
+
t.Fatal("expected request_id to be set by RequestID middleware")
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// TestEchoParams_EmptyName registers EchoParams on a param-free route so that
|
|
1874
|
+
// c.Params("name") returns "" and the 400 guard executes.
|
|
1875
|
+
func TestEchoParams_EmptyName(t *testing.T) {
|
|
1876
|
+
app := fiber.New()
|
|
1877
|
+
app.Get("/echo-bare", handlers.EchoParams)
|
|
1878
|
+
req := httptest.NewRequest(http.MethodGet, "/echo-bare", nil)
|
|
1879
|
+
resp, err := app.Test(req, -1)
|
|
1880
|
+
if err != nil {
|
|
1881
|
+
t.Fatalf("request error: %v", err)
|
|
1882
|
+
}
|
|
1883
|
+
defer resp.Body.Close()
|
|
1884
|
+
|
|
1885
|
+
if resp.StatusCode != http.StatusBadRequest {
|
|
1886
|
+
t.Fatalf("expected 400, got %d", resp.StatusCode)
|
|
1887
|
+
}
|
|
1888
|
+
var body map[string]any
|
|
1889
|
+
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
1890
|
+
t.Fatalf("invalid JSON: %v", err)
|
|
1891
|
+
}
|
|
1892
|
+
if body["code"] != "BAD_REQUEST" {
|
|
1893
|
+
t.Fatalf("expected code=BAD_REQUEST, got %v", body["code"])
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
`}function Or(e){return `package config_test
|
|
1897
|
+
|
|
1898
|
+
import (
|
|
1899
|
+
"log/slog"
|
|
1900
|
+
"testing"
|
|
1901
|
+
|
|
1902
|
+
"${e.module_path}/internal/config"
|
|
1903
|
+
)
|
|
1904
|
+
|
|
1905
|
+
func TestParseLogLevel(t *testing.T) {
|
|
1906
|
+
tests := []struct {
|
|
1907
|
+
input string
|
|
1908
|
+
want slog.Level
|
|
1909
|
+
}{
|
|
1910
|
+
{"debug", slog.LevelDebug},
|
|
1911
|
+
{"DEBUG", slog.LevelDebug},
|
|
1912
|
+
{"warn", slog.LevelWarn},
|
|
1913
|
+
{"warning", slog.LevelWarn},
|
|
1914
|
+
{"error", slog.LevelError},
|
|
1915
|
+
{"info", slog.LevelInfo},
|
|
1916
|
+
{"", slog.LevelInfo},
|
|
1917
|
+
{"unknown", slog.LevelInfo},
|
|
1918
|
+
}
|
|
1919
|
+
for _, tc := range tests {
|
|
1920
|
+
got := config.ParseLogLevel(tc.input)
|
|
1921
|
+
if got != tc.want {
|
|
1922
|
+
t.Errorf("ParseLogLevel(%q) = %v, want %v", tc.input, got, tc.want)
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
func TestLoad_EnvOverride(t *testing.T) {
|
|
1928
|
+
t.Setenv("PORT", "9090")
|
|
1929
|
+
t.Setenv("APP_ENV", "production")
|
|
1930
|
+
t.Setenv("LOG_LEVEL", "warn")
|
|
1931
|
+
|
|
1932
|
+
cfg := config.Load()
|
|
1933
|
+
|
|
1934
|
+
if cfg.Port != "9090" {
|
|
1935
|
+
t.Errorf("expected Port=9090, got %q", cfg.Port)
|
|
1936
|
+
}
|
|
1937
|
+
if cfg.Env != "production" {
|
|
1938
|
+
t.Errorf("expected Env=production, got %q", cfg.Env)
|
|
1939
|
+
}
|
|
1940
|
+
if cfg.LogLevel != "warn" {
|
|
1941
|
+
t.Errorf("expected LogLevel=warn, got %q", cfg.LogLevel)
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
func TestLoad_Defaults(t *testing.T) {
|
|
1946
|
+
// Empty string forces getEnv() to return the built-in fallback value.
|
|
1947
|
+
t.Setenv("PORT", "")
|
|
1948
|
+
t.Setenv("APP_ENV", "")
|
|
1949
|
+
t.Setenv("LOG_LEVEL", "")
|
|
1950
|
+
|
|
1951
|
+
cfg := config.Load()
|
|
1952
|
+
|
|
1953
|
+
if cfg.Port != "${e.port}" {
|
|
1954
|
+
t.Errorf("expected default Port=${e.port}, got %q", cfg.Port)
|
|
1955
|
+
}
|
|
1956
|
+
if cfg.Env != "development" {
|
|
1957
|
+
t.Errorf("expected default Env=development, got %q", cfg.Env)
|
|
1958
|
+
}
|
|
1959
|
+
// APP_ENV="" \u2192 fallback "development" \u2192 defaultLogLevel \u2192 "debug"
|
|
1960
|
+
if cfg.LogLevel != "debug" {
|
|
1961
|
+
t.Errorf("expected default LogLevel=debug (development env), got %q", cfg.LogLevel)
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
`}function Nr(){return `package middleware
|
|
1965
|
+
|
|
1966
|
+
import (
|
|
1967
|
+
"os"
|
|
1968
|
+
|
|
1969
|
+
"github.com/gofiber/fiber/v2"
|
|
1970
|
+
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
1971
|
+
)
|
|
1972
|
+
|
|
1973
|
+
// CORS returns a CORS middleware configured via CORS_ALLOW_ORIGINS env var.
|
|
1974
|
+
//
|
|
1975
|
+
// Set CORS_ALLOW_ORIGINS="*" for development (the default when unset).
|
|
1976
|
+
// In production supply a comma-separated list of allowed origins:
|
|
1977
|
+
//
|
|
1978
|
+
// CORS_ALLOW_ORIGINS=https://app.example.com,https://admin.example.com
|
|
1979
|
+
func CORS() fiber.Handler {
|
|
1980
|
+
origins := os.Getenv("CORS_ALLOW_ORIGINS")
|
|
1981
|
+
if origins == "" {
|
|
1982
|
+
origins = "*"
|
|
1983
|
+
}
|
|
1984
|
+
return cors.New(cors.Config{
|
|
1985
|
+
AllowOrigins: origins,
|
|
1986
|
+
AllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS",
|
|
1987
|
+
AllowHeaders: "Origin,Content-Type,Authorization,X-Request-ID",
|
|
1988
|
+
ExposeHeaders: "X-Request-ID",
|
|
1989
|
+
MaxAge: 600,
|
|
1990
|
+
})
|
|
1991
|
+
}
|
|
1992
|
+
`}function jr(e){return `package middleware_test
|
|
1993
|
+
|
|
1994
|
+
import (
|
|
1995
|
+
"net/http"
|
|
1996
|
+
"net/http/httptest"
|
|
1997
|
+
"testing"
|
|
1998
|
+
|
|
1999
|
+
"github.com/gofiber/fiber/v2"
|
|
2000
|
+
|
|
2001
|
+
"${e.module_path}/internal/middleware"
|
|
2002
|
+
)
|
|
2003
|
+
|
|
2004
|
+
func newCORSApp(t *testing.T) *fiber.App {
|
|
2005
|
+
t.Helper()
|
|
2006
|
+
app := fiber.New()
|
|
2007
|
+
app.Use(middleware.CORS())
|
|
2008
|
+
app.Get("/ping", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
|
2009
|
+
return app
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
func TestCORS_Wildcard(t *testing.T) {
|
|
2013
|
+
t.Setenv("CORS_ALLOW_ORIGINS", "*")
|
|
2014
|
+
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
2015
|
+
req.Header.Set("Origin", "https://example.com")
|
|
2016
|
+
resp, _ := newCORSApp(t).Test(req, -1)
|
|
2017
|
+
defer resp.Body.Close()
|
|
2018
|
+
|
|
2019
|
+
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "*" {
|
|
2020
|
+
t.Fatalf("expected ACAO=*, got %q", got)
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
func TestCORS_Preflight(t *testing.T) {
|
|
2025
|
+
t.Setenv("CORS_ALLOW_ORIGINS", "*")
|
|
2026
|
+
app := fiber.New()
|
|
2027
|
+
app.Use(middleware.CORS())
|
|
2028
|
+
|
|
2029
|
+
req := httptest.NewRequest(http.MethodOptions, "/ping", nil)
|
|
2030
|
+
req.Header.Set("Origin", "https://example.com")
|
|
2031
|
+
req.Header.Set("Access-Control-Request-Method", "POST")
|
|
2032
|
+
resp, _ := app.Test(req, -1)
|
|
2033
|
+
defer resp.Body.Close()
|
|
2034
|
+
|
|
2035
|
+
if resp.StatusCode != http.StatusNoContent {
|
|
2036
|
+
t.Fatalf("expected 204 preflight, got %d", resp.StatusCode)
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
func TestCORS_SpecificOrigin_Allowed(t *testing.T) {
|
|
2041
|
+
t.Setenv("CORS_ALLOW_ORIGINS", "https://app.example.com")
|
|
2042
|
+
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
2043
|
+
req.Header.Set("Origin", "https://app.example.com")
|
|
2044
|
+
resp, _ := newCORSApp(t).Test(req, -1)
|
|
2045
|
+
defer resp.Body.Close()
|
|
2046
|
+
|
|
2047
|
+
if got := resp.Header.Get("Access-Control-Allow-Origin"); got != "https://app.example.com" {
|
|
2048
|
+
t.Fatalf("expected ACAO=https://app.example.com, got %q", got)
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
func TestCORS_Default_Origin(t *testing.T) {
|
|
2053
|
+
// When CORS_ALLOW_ORIGINS is unset, middleware must default to "*".
|
|
2054
|
+
t.Setenv("CORS_ALLOW_ORIGINS", "")
|
|
2055
|
+
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
|
|
2056
|
+
req.Header.Set("Origin", "https://anywhere.com")
|
|
2057
|
+
resp, _ := newCORSApp(t).Test(req, -1)
|
|
2058
|
+
defer resp.Body.Close()
|
|
2059
|
+
|
|
2060
|
+
if got := resp.Header.Get("Access-Control-Allow-Origin"); got == "" {
|
|
2061
|
+
t.Fatal("expected CORS header when origins defaulting to *")
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
`}function Gr(e){return `package server_test
|
|
2065
|
+
|
|
2066
|
+
import (
|
|
2067
|
+
"encoding/json"
|
|
2068
|
+
"net/http"
|
|
2069
|
+
"net/http/httptest"
|
|
2070
|
+
"testing"
|
|
2071
|
+
|
|
2072
|
+
"${e.module_path}/internal/config"
|
|
2073
|
+
"${e.module_path}/internal/server"
|
|
2074
|
+
)
|
|
2075
|
+
|
|
2076
|
+
type serverAPIError struct {
|
|
2077
|
+
Code string \`json:"code"\`
|
|
2078
|
+
Message string \`json:"message"\`
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
func TestServer_NotFound_JSON(t *testing.T) {
|
|
2082
|
+
req := httptest.NewRequest(http.MethodGet, "/no-such-route", nil)
|
|
2083
|
+
resp, err := server.NewApp(config.Load()).Test(req, -1)
|
|
2084
|
+
if err != nil {
|
|
2085
|
+
t.Fatalf("request error: %v", err)
|
|
2086
|
+
}
|
|
2087
|
+
defer resp.Body.Close()
|
|
2088
|
+
|
|
2089
|
+
if resp.StatusCode != http.StatusNotFound {
|
|
2090
|
+
t.Fatalf("expected 404, got %d", resp.StatusCode)
|
|
2091
|
+
}
|
|
2092
|
+
var body serverAPIError
|
|
2093
|
+
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
2094
|
+
t.Fatalf("expected JSON error body: %v", err)
|
|
2095
|
+
}
|
|
2096
|
+
if body.Code != "NOT_FOUND" {
|
|
2097
|
+
t.Fatalf("expected code=NOT_FOUND, got %q", body.Code)
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
func TestServer_MethodNotAllowed_JSON(t *testing.T) {
|
|
2102
|
+
// Fiber v2 does not return 405 automatically \u2014 unmatched methods fall
|
|
2103
|
+
// through to the 404 catch-all, which is the expected behaviour.
|
|
2104
|
+
req := httptest.NewRequest(http.MethodPost, "/api/v1/health/live", nil)
|
|
2105
|
+
resp, err := server.NewApp(config.Load()).Test(req, -1)
|
|
2106
|
+
if err != nil {
|
|
2107
|
+
t.Fatalf("request error: %v", err)
|
|
2108
|
+
}
|
|
2109
|
+
defer resp.Body.Close()
|
|
2110
|
+
|
|
2111
|
+
if resp.StatusCode != http.StatusNotFound {
|
|
2112
|
+
t.Fatalf("expected 404 for unmatched method, got %d", resp.StatusCode)
|
|
2113
|
+
}
|
|
2114
|
+
var body serverAPIError
|
|
2115
|
+
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
2116
|
+
t.Fatalf("expected JSON error body: %v", err)
|
|
2117
|
+
}
|
|
2118
|
+
if body.Code != "NOT_FOUND" {
|
|
2119
|
+
t.Fatalf("expected code=NOT_FOUND, got %q", body.Code)
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
func TestServer_CORS_Header(t *testing.T) {
|
|
2124
|
+
t.Setenv("CORS_ALLOW_ORIGINS", "*")
|
|
2125
|
+
req := httptest.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
|
|
2126
|
+
req.Header.Set("Origin", "https://example.com")
|
|
2127
|
+
resp, err := server.NewApp(config.Load()).Test(req, -1)
|
|
2128
|
+
if err != nil {
|
|
2129
|
+
t.Fatalf("request error: %v", err)
|
|
2130
|
+
}
|
|
2131
|
+
defer resp.Body.Close()
|
|
2132
|
+
|
|
2133
|
+
if got := resp.Header.Get("Access-Control-Allow-Origin"); got == "" {
|
|
2134
|
+
t.Fatal("expected Access-Control-Allow-Origin header to be set")
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
func TestServer_Docs_Redirect(t *testing.T) {
|
|
2139
|
+
req := httptest.NewRequest(http.MethodGet, "/docs", nil)
|
|
2140
|
+
resp, err := server.NewApp(config.Load()).Test(req, -1)
|
|
2141
|
+
if err != nil {
|
|
2142
|
+
t.Fatalf("request error: %v", err)
|
|
2143
|
+
}
|
|
2144
|
+
defer resp.Body.Close()
|
|
2145
|
+
|
|
2146
|
+
if resp.StatusCode != http.StatusFound {
|
|
2147
|
+
t.Fatalf("expected 302 redirect from /docs, got %d", resp.StatusCode)
|
|
2148
|
+
}
|
|
2149
|
+
if loc := resp.Header.Get("Location"); loc != "/docs/index.html" {
|
|
2150
|
+
t.Fatalf("expected Location=/docs/index.html, got %q", loc)
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
`}function $r(e){return `package middleware
|
|
2154
|
+
|
|
2155
|
+
import (
|
|
2156
|
+
"os"
|
|
2157
|
+
"strconv"
|
|
2158
|
+
"time"
|
|
2159
|
+
|
|
2160
|
+
"github.com/gofiber/fiber/v2"
|
|
2161
|
+
"github.com/gofiber/fiber/v2/middleware/limiter"
|
|
2162
|
+
|
|
2163
|
+
"${e.module_path}/internal/apierr"
|
|
2164
|
+
)
|
|
2165
|
+
|
|
2166
|
+
// RateLimit returns a per-IP sliding-window rate limiter.
|
|
2167
|
+
// Configure the limit via RATE_LIMIT_RPS env var (requests per second, default 100).
|
|
2168
|
+
func RateLimit() fiber.Handler {
|
|
2169
|
+
rps := 100
|
|
2170
|
+
if raw := os.Getenv("RATE_LIMIT_RPS"); raw != "" {
|
|
2171
|
+
if n, err := strconv.Atoi(raw); err == nil && n > 0 {
|
|
2172
|
+
rps = n
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return limiter.New(limiter.Config{
|
|
2176
|
+
Max: rps,
|
|
2177
|
+
Expiration: time.Second,
|
|
2178
|
+
KeyGenerator: func(c *fiber.Ctx) string {
|
|
2179
|
+
return c.IP()
|
|
2180
|
+
},
|
|
2181
|
+
LimitReached: func(c *fiber.Ctx) error {
|
|
2182
|
+
return apierr.TooManyRequests(c, "rate limit exceeded")
|
|
2183
|
+
},
|
|
2184
|
+
})
|
|
2185
|
+
}
|
|
2186
|
+
`}function Dr(e){return `package middleware_test
|
|
2187
|
+
|
|
2188
|
+
import (
|
|
2189
|
+
"net/http"
|
|
2190
|
+
"net/http/httptest"
|
|
2191
|
+
"testing"
|
|
2192
|
+
|
|
2193
|
+
"github.com/gofiber/fiber/v2"
|
|
2194
|
+
|
|
2195
|
+
"${e.module_path}/internal/middleware"
|
|
2196
|
+
)
|
|
2197
|
+
|
|
2198
|
+
func newRateLimitApp(t *testing.T) *fiber.App {
|
|
2199
|
+
t.Helper()
|
|
2200
|
+
app := fiber.New()
|
|
2201
|
+
app.Use(middleware.RateLimit())
|
|
2202
|
+
app.Get("/", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })
|
|
2203
|
+
return app
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
func TestRateLimit_AllowsUnderLimit(t *testing.T) {
|
|
2207
|
+
t.Setenv("RATE_LIMIT_RPS", "3")
|
|
2208
|
+
app := newRateLimitApp(t)
|
|
2209
|
+
|
|
2210
|
+
for i := 0; i < 3; i++ {
|
|
2211
|
+
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
2212
|
+
resp, err := app.Test(req, -1)
|
|
2213
|
+
if err != nil {
|
|
2214
|
+
t.Fatalf("request %d: %v", i+1, err)
|
|
2215
|
+
}
|
|
2216
|
+
resp.Body.Close()
|
|
2217
|
+
if resp.StatusCode != http.StatusOK {
|
|
2218
|
+
t.Fatalf("request %d: expected 200, got %d", i+1, resp.StatusCode)
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
func TestRateLimit_Blocks_After_Limit(t *testing.T) {
|
|
2224
|
+
t.Setenv("RATE_LIMIT_RPS", "2")
|
|
2225
|
+
app := newRateLimitApp(t)
|
|
2226
|
+
|
|
2227
|
+
// Exhaust the limit.
|
|
2228
|
+
for i := 0; i < 2; i++ {
|
|
2229
|
+
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
2230
|
+
resp, _ := app.Test(req, -1)
|
|
2231
|
+
resp.Body.Close()
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
// Next request must be rejected.
|
|
2235
|
+
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
2236
|
+
resp, err := app.Test(req, -1)
|
|
2237
|
+
if err != nil {
|
|
2238
|
+
t.Fatalf("over-limit request: %v", err)
|
|
2239
|
+
}
|
|
2240
|
+
defer resp.Body.Close()
|
|
2241
|
+
|
|
2242
|
+
if resp.StatusCode != http.StatusTooManyRequests {
|
|
2243
|
+
t.Fatalf("expected 429, got %d", resp.StatusCode)
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
func TestRateLimit_InvalidRPS(t *testing.T) {
|
|
2248
|
+
// Invalid value should fall back to default (100 rps) and allow normal requests.
|
|
2249
|
+
t.Setenv("RATE_LIMIT_RPS", "not-a-number")
|
|
2250
|
+
app := newRateLimitApp(t)
|
|
2251
|
+
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
2252
|
+
resp, err := app.Test(req, -1)
|
|
2253
|
+
if err != nil {
|
|
2254
|
+
t.Fatalf("request error: %v", err)
|
|
2255
|
+
}
|
|
2256
|
+
defer resp.Body.Close()
|
|
2257
|
+
|
|
2258
|
+
if resp.StatusCode != http.StatusOK {
|
|
2259
|
+
t.Fatalf("expected 200 with invalid RPS env, got %d", resp.StatusCode)
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
`}function Mr(e){return `# Air \u2014 live reload for Go projects
|
|
2263
|
+
# https://github.com/air-verse/air
|
|
2264
|
+
root = "."
|
|
2265
|
+
tmp_dir = "tmp"
|
|
2266
|
+
|
|
2267
|
+
[build]
|
|
2268
|
+
pre_cmd = ["$(go env GOPATH)/bin/swag init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null || true"]
|
|
2269
|
+
cmd = "go build -o ./tmp/server ./cmd/server"
|
|
2270
|
+
bin = "./tmp/server"
|
|
2271
|
+
include_ext = ["go", "yaml", "yml", "env"]
|
|
2272
|
+
exclude_dir = ["tmp", "vendor", ".git", "testdata", "docs"]
|
|
2273
|
+
delay = 500
|
|
2274
|
+
rerun_delay = 500
|
|
2275
|
+
send_interrupt = true
|
|
2276
|
+
kill_delay = "200ms"
|
|
2277
|
+
|
|
2278
|
+
[env]
|
|
2279
|
+
PORT = "${e.port}"
|
|
2280
|
+
|
|
2281
|
+
[misc]
|
|
2282
|
+
clean_on_exit = true
|
|
2283
|
+
|
|
2284
|
+
[log]
|
|
2285
|
+
time = false
|
|
2286
|
+
`}function qr(e){return `run:
|
|
2287
|
+
timeout: 5m
|
|
2288
|
+
|
|
2289
|
+
linters:
|
|
2290
|
+
enable:
|
|
2291
|
+
- bodyclose
|
|
2292
|
+
- durationcheck
|
|
2293
|
+
- errcheck
|
|
2294
|
+
- errname
|
|
2295
|
+
- errorlint
|
|
2296
|
+
- gci
|
|
2297
|
+
- goimports
|
|
2298
|
+
- gosimple
|
|
2299
|
+
- govet
|
|
2300
|
+
- ineffassign
|
|
2301
|
+
- misspell
|
|
2302
|
+
- noctx
|
|
2303
|
+
- nolintlint
|
|
2304
|
+
- prealloc
|
|
2305
|
+
- staticcheck
|
|
2306
|
+
- unconvert
|
|
2307
|
+
- unused
|
|
2308
|
+
- wrapcheck
|
|
2309
|
+
|
|
2310
|
+
linters-settings:
|
|
2311
|
+
gci:
|
|
2312
|
+
sections:
|
|
2313
|
+
- standard
|
|
2314
|
+
- default
|
|
2315
|
+
- prefix(${e})
|
|
2316
|
+
goimports:
|
|
2317
|
+
local-prefixes: "${e}"
|
|
2318
|
+
govet:
|
|
2319
|
+
enable:
|
|
2320
|
+
- shadow
|
|
2321
|
+
wrapcheck:
|
|
2322
|
+
ignorePackageGlobs:
|
|
2323
|
+
- "${e}/*"
|
|
2324
|
+
|
|
2325
|
+
issues:
|
|
2326
|
+
max-same-issues: 5
|
|
2327
|
+
exclude-rules:
|
|
2328
|
+
- path: _test.go
|
|
2329
|
+
linters:
|
|
2330
|
+
- errcheck
|
|
2331
|
+
- wrapcheck
|
|
2332
|
+
`}function Lr(){return JSON.stringify({engine:"npm",runtime:"go"},null,2)}function Fr(e){return `#!/usr/bin/env sh
|
|
2333
|
+
# RapidKit Go/Fiber project launcher \u2014 generated by RapidKit CLI
|
|
2334
|
+
# https://getrapidkit.com
|
|
2335
|
+
|
|
2336
|
+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
2337
|
+
CMD="\${1:-}"
|
|
2338
|
+
shift 2>/dev/null || true
|
|
2339
|
+
|
|
2340
|
+
case "$CMD" in
|
|
2341
|
+
init)
|
|
2342
|
+
cd "$SCRIPT_DIR"
|
|
2343
|
+
echo "\u{1F439} Initializing Go/Fiber project\u2026"
|
|
2344
|
+
GOBIN="$(go env GOPATH)/bin"
|
|
2345
|
+
echo " \u2192 installing air (hot reload)\u2026"
|
|
2346
|
+
go install github.com/air-verse/air@latest 2>/dev/null && echo " \u2713 air" || echo " \u26A0 air install failed (run: go install github.com/air-verse/air@latest)"
|
|
2347
|
+
echo " \u2192 installing swag (swagger)\u2026"
|
|
2348
|
+
go install github.com/swaggo/swag/cmd/swag@latest 2>/dev/null && echo " \u2713 swag" || echo " \u26A0 swag install failed (run: go install github.com/swaggo/swag/cmd/swag@latest)"
|
|
2349
|
+
if [ ! -f ".env" ] && [ -f ".env.example" ]; then
|
|
2350
|
+
cp .env.example .env && echo " \u2713 .env created from .env.example"
|
|
2351
|
+
fi
|
|
2352
|
+
go mod tidy && echo " \u2713 go mod tidy"
|
|
2353
|
+
echo " \u2192 generating swagger docs (first build)\u2026"
|
|
2354
|
+
"$(go env GOPATH)/bin/swag" init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null && echo " \u2713 swagger docs generated" || echo " \u26A0 swagger docs skipped (run: rapidkit docs)"
|
|
2355
|
+
echo "\u2705 Ready \u2014 run: rapidkit dev"
|
|
2356
|
+
;;
|
|
2357
|
+
dev)
|
|
2358
|
+
cd "$SCRIPT_DIR"
|
|
2359
|
+
echo "\u{1F4D6} Syncing swagger docs\u2026"
|
|
2360
|
+
"$(go env GOPATH)/bin/swag" init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null || true
|
|
2361
|
+
if [ -f "$SCRIPT_DIR/Makefile" ]; then
|
|
2362
|
+
exec make -C "$SCRIPT_DIR" dev "$@"
|
|
2363
|
+
else
|
|
2364
|
+
exec go run ./cmd/server "$@"
|
|
2365
|
+
fi
|
|
2366
|
+
;;
|
|
2367
|
+
start)
|
|
2368
|
+
BIN="$SCRIPT_DIR/bin/${e.project_name}"
|
|
2369
|
+
if [ ! -f "$BIN" ]; then
|
|
2370
|
+
make -C "$SCRIPT_DIR" build
|
|
2371
|
+
fi
|
|
2372
|
+
exec "$BIN" "$@"
|
|
2373
|
+
;;
|
|
2374
|
+
build)
|
|
2375
|
+
exec make -C "$SCRIPT_DIR" build "$@"
|
|
2376
|
+
;;
|
|
2377
|
+
test)
|
|
2378
|
+
exec make -C "$SCRIPT_DIR" test "$@"
|
|
2379
|
+
;;
|
|
2380
|
+
lint)
|
|
2381
|
+
exec make -C "$SCRIPT_DIR" lint "$@"
|
|
2382
|
+
;;
|
|
2383
|
+
format|fmt)
|
|
2384
|
+
exec make -C "$SCRIPT_DIR" fmt "$@"
|
|
2385
|
+
;;
|
|
2386
|
+
docs)
|
|
2387
|
+
exec make -C "$SCRIPT_DIR" docs "$@"
|
|
2388
|
+
;;
|
|
2389
|
+
help|--help|-h)
|
|
2390
|
+
echo "RapidKit \u2014 Go/Fiber project: ${e.project_name}"
|
|
2391
|
+
echo ""
|
|
2392
|
+
echo "Usage: rapidkit <command>"
|
|
2393
|
+
echo ""
|
|
2394
|
+
echo " init Install tools + create .env (air, swag, go mod tidy)"
|
|
2395
|
+
echo " dev Hot reload dev server (make dev \u2014 requires air)"
|
|
2396
|
+
echo " start Run compiled binary (make build + bin)"
|
|
2397
|
+
echo " build Build binary (make build)"
|
|
2398
|
+
echo " docs Generate Swagger docs (make docs \u2014 requires swag)"
|
|
2399
|
+
echo " test Run tests (make test)"
|
|
2400
|
+
echo " lint Run linter (make lint)"
|
|
2401
|
+
echo " format Format code (make fmt)"
|
|
2402
|
+
;;
|
|
2403
|
+
*)
|
|
2404
|
+
if [ -n "$CMD" ]; then
|
|
2405
|
+
echo "rapidkit: unknown command: $CMD" >&2
|
|
2406
|
+
fi
|
|
2407
|
+
echo "Available: init, dev, start, build, docs, test, lint, format" >&2
|
|
2408
|
+
exit 1
|
|
2409
|
+
;;
|
|
2410
|
+
esac
|
|
2411
|
+
`}function Hr(e){return `@echo off
|
|
2412
|
+
rem RapidKit Go/Fiber project launcher \u2014 Windows
|
|
2413
|
+
set CMD=%1
|
|
2414
|
+
if "%CMD%"=="" goto usage
|
|
2415
|
+
shift
|
|
2416
|
+
|
|
2417
|
+
if "%CMD%"=="init" (
|
|
2418
|
+
echo Initializing Go/Fiber project...
|
|
2419
|
+
go install github.com/air-verse/air@latest
|
|
2420
|
+
go install github.com/swaggo/swag/cmd/swag@latest
|
|
2421
|
+
if not exist .env if exist .env.example copy .env.example .env
|
|
2422
|
+
go mod tidy
|
|
2423
|
+
exit /b %ERRORLEVEL%
|
|
2424
|
+
)
|
|
2425
|
+
if "%CMD%"=="dev" ( make dev %* & exit /b %ERRORLEVEL% )
|
|
2426
|
+
if "%CMD%"=="build" ( make build %* & exit /b %ERRORLEVEL% )
|
|
2427
|
+
if "%CMD%"=="test" ( make test %* & exit /b %ERRORLEVEL% )
|
|
2428
|
+
if "%CMD%"=="lint" ( make lint %* & exit /b %ERRORLEVEL% )
|
|
2429
|
+
if "%CMD%"=="format" ( make fmt %* & exit /b %ERRORLEVEL% )
|
|
2430
|
+
if "%CMD%"=="docs" ( make docs %* & exit /b %ERRORLEVEL% )
|
|
2431
|
+
if "%CMD%"=="start" ( bin\\${e.project_name}.exe %* & exit /b %ERRORLEVEL% )
|
|
2432
|
+
|
|
2433
|
+
:usage
|
|
2434
|
+
echo Available: init, dev, start, build, docs, test, lint, format
|
|
2435
|
+
exit /b 1
|
|
2436
|
+
`}function Kr(e,o){return JSON.stringify({kit_name:"gofiber.standard",runtime:"go",module_support:false,project_name:e.project_name,module_path:e.module_path,app_version:e.app_version,created_by:"rapidkit-npm",rapidkit_version:o,created_at:new Date().toISOString()},null,2)}async function gt(e,o){let t={project_name:o.project_name,module_path:o.module_path||o.project_name,author:o.author||"RapidKit User",description:o.description||`Go/Fiber REST API \u2014 ${o.project_name}`,go_version:o.go_version||"1.24",app_version:o.app_version||"0.1.0",port:o.port||"3000",skipGit:o.skipGit??false},r=c();try{await execa("go",["version"],{timeout:3e3});}catch{console.log(g.yellow("\n\u26A0 Go not found in PATH \u2014 project will be scaffolded, but `go mod tidy` requires Go 1.21+")),console.log(g.gray(` Install: https://go.dev/dl/
|
|
2437
|
+
`));}let i=pt(`Generating Go/Fiber project: ${t.project_name}\u2026`).start();try{let n=(a,l)=>pr(v.join(e,a),l),s=v.join(e,"rapidkit"),c=v.join(e,"rapidkit.cmd");await Promise.all([n("cmd/server/main.go",ur(t)),n("go.mod",gr(t)),n("internal/config/config.go",mr(t)),n("internal/server/server.go",fr(t)),n("internal/middleware/requestid.go",Sr()),n("internal/middleware/requestid_test.go",_r(t)),n("internal/apierr/apierr.go",Pr()),n("internal/apierr/apierr_test.go",Ir(t)),n("internal/handlers/health.go",hr()),n("internal/handlers/health_test.go",wr(t)),n("internal/handlers/example.go",Tr(t)),n("internal/handlers/example_test.go",Ar(t)),n("internal/config/config_test.go",Or(t)),n("internal/middleware/cors.go",Nr()),n("internal/middleware/cors_test.go",jr(t)),n("internal/middleware/ratelimit.go",$r(t)),n("internal/middleware/ratelimit_test.go",Dr(t)),n("internal/server/server_test.go",Gr(t)),n("docs/doc.go",Er(t)),n(".air.toml",Mr(t)),n("Dockerfile",yr()),n("docker-compose.yml",vr(t)),n("Makefile",br(t)),n(".golangci.yml",qr(t.module_path)),n(".env.example",kr(t)),n(".gitignore",Rr()),n(".github/workflows/ci.yml",xr(t)),n("README.md",Cr(t)),n(".rapidkit/project.json",Kr(t,r)),n(".rapidkit/context.json",Lr()),n("rapidkit",Fr(t)),n("rapidkit.cmd",Hr(t))]),await promises.chmod(s,493),await promises.chmod(c,493),i.succeed(g.green(`Project created at ${e}`));try{i.start("Fetching Go dependencies\u2026"),await execa("go",["mod","tidy"],{cwd:e,timeout:12e4}),i.succeed(g.gray("\u2713 go mod tidy completed"));}catch{i.warn(g.yellow("\u26A0 go mod tidy failed \u2014 run manually: go mod tidy"));}if(!t.skipGit)try{await execa("git",["init"],{cwd:e}),await execa("git",["add","-A"],{cwd:e}),await execa("git",["commit","-m","chore: initial scaffold (rapidkit gofiber.standard)"],{cwd:e}),console.log(g.gray("\u2713 git repository initialized"));}catch{console.log(g.gray("\u26A0 git init skipped (git not found or error)"));}console.log(""),console.log(g.bold("\u2705 Go/Fiber project ready!")),console.log(""),console.log(g.cyan("Next steps:")),console.log(g.white(` cd ${t.project_name}`)),console.log(g.white(" make run # start dev server")),console.log(g.white(" make test # run tests")),console.log(""),console.log(g.gray("Server will listen on port "+t.port)),console.log(g.gray(" http://localhost:"+t.port+"/api/v1/health/live")),console.log(g.gray(" http://localhost:"+t.port+"/api/v1/health/ready")),console.log(""),console.log(g.yellow("\u2139 RapidKit modules are not available for Go projects (module system uses Python/pip).")),console.log("");}catch(n){throw i.fail(g.red("Failed to generate Go/Fiber project")),n}}function Jt(e){return e.split(/[-_\s]+/).map(o=>o.charAt(0).toUpperCase()+o.slice(1)).join("")}async function Vr(e,o){await promises.mkdir(v.dirname(e),{recursive:true}),await promises.writeFile(e,o,"utf8");}function Wr(e){return `package main
|
|
2438
|
+
|
|
2439
|
+
import (
|
|
2440
|
+
"context"
|
|
2441
|
+
"errors"
|
|
2442
|
+
"fmt"
|
|
2443
|
+
"log/slog"
|
|
2444
|
+
"net/http"
|
|
2445
|
+
"os"
|
|
2446
|
+
"os/signal"
|
|
2447
|
+
"syscall"
|
|
2448
|
+
"time"
|
|
2449
|
+
|
|
2450
|
+
_ "${e.module_path}/docs"
|
|
2451
|
+
"${e.module_path}/internal/config"
|
|
2452
|
+
"${e.module_path}/internal/server"
|
|
2453
|
+
)
|
|
2454
|
+
|
|
2455
|
+
// Build-time variables \u2014 injected via -ldflags.
|
|
2456
|
+
var (
|
|
2457
|
+
version = "dev"
|
|
2458
|
+
commit = "none"
|
|
2459
|
+
date = "unknown"
|
|
2460
|
+
)
|
|
2461
|
+
|
|
2462
|
+
func main() {
|
|
2463
|
+
cfg := config.Load()
|
|
2464
|
+
|
|
2465
|
+
log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
2466
|
+
Level: config.ParseLogLevel(cfg.LogLevel),
|
|
2467
|
+
}))
|
|
2468
|
+
slog.SetDefault(log)
|
|
2469
|
+
|
|
2470
|
+
r := server.NewRouter(cfg)
|
|
2471
|
+
|
|
2472
|
+
srv := &http.Server{
|
|
2473
|
+
Addr: ":" + cfg.Port,
|
|
2474
|
+
Handler: r,
|
|
2475
|
+
ReadTimeout: 5 * time.Second,
|
|
2476
|
+
WriteTimeout: 10 * time.Second,
|
|
2477
|
+
IdleTimeout: 30 * time.Second,
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
go func() {
|
|
2481
|
+
slog.Info("starting", "port", cfg.Port, "version", version, "commit", commit, "date", date, "env", cfg.Env)
|
|
2482
|
+
fmt.Printf("\\n\u{1F680} Server \u2192 http://127.0.0.1:%s\\n", cfg.Port)
|
|
2483
|
+
fmt.Printf("\u{1F4D6} Docs \u2192 http://127.0.0.1:%s/docs\\n\\n", cfg.Port)
|
|
2484
|
+
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
2485
|
+
slog.Error("server error", "err", err)
|
|
2486
|
+
os.Exit(1)
|
|
2487
|
+
}
|
|
2488
|
+
}()
|
|
2489
|
+
|
|
2490
|
+
quit := make(chan os.Signal, 1)
|
|
2491
|
+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
2492
|
+
<-quit
|
|
2493
|
+
|
|
2494
|
+
slog.Info("shutting down\u2026")
|
|
2495
|
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
2496
|
+
defer cancel()
|
|
2497
|
+
|
|
2498
|
+
if err := srv.Shutdown(ctx); err != nil {
|
|
2499
|
+
slog.Error("forced shutdown", "err", err)
|
|
2500
|
+
os.Exit(1)
|
|
2501
|
+
}
|
|
2502
|
+
slog.Info("server stopped")
|
|
2503
|
+
}
|
|
2504
|
+
`}function Br(e){return `module ${e.module_path}
|
|
2505
|
+
|
|
2506
|
+
go ${e.go_version}
|
|
2507
|
+
|
|
2508
|
+
require (
|
|
2509
|
+
github.com/gin-gonic/gin v1.10.0
|
|
2510
|
+
github.com/swaggo/gin-swagger v1.6.0
|
|
2511
|
+
github.com/swaggo/swag v1.16.3
|
|
2512
|
+
)
|
|
2513
|
+
|
|
2514
|
+
require (
|
|
2515
|
+
github.com/KyleBanks/depth v1.2.1 // indirect
|
|
2516
|
+
github.com/bytedance/sonic v1.11.6 // indirect
|
|
2517
|
+
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
|
2518
|
+
github.com/cloudwego/base64x v0.1.4 // indirect
|
|
2519
|
+
github.com/cloudwego/iasm v0.2.0 // indirect
|
|
2520
|
+
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
|
2521
|
+
github.com/ghodss/yaml v1.0.0 // indirect
|
|
2522
|
+
github.com/gin-contrib/sse v0.1.0 // indirect
|
|
2523
|
+
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
|
2524
|
+
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
|
2525
|
+
github.com/go-openapi/spec v0.21.0 // indirect
|
|
2526
|
+
github.com/go-openapi/swag v0.23.0 // indirect
|
|
2527
|
+
github.com/go-playground/locales v0.14.1 // indirect
|
|
2528
|
+
github.com/go-playground/universal-translator v0.18.1 // indirect
|
|
2529
|
+
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
|
2530
|
+
github.com/goccy/go-json v0.10.2 // indirect
|
|
2531
|
+
github.com/josharian/intern v1.0.0 // indirect
|
|
2532
|
+
github.com/json-iterator/go v1.1.12 // indirect
|
|
2533
|
+
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
|
2534
|
+
github.com/leodido/go-urn v1.4.0 // indirect
|
|
2535
|
+
github.com/mailru/easyjson v0.7.7 // indirect
|
|
2536
|
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
2537
|
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
2538
|
+
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
2539
|
+
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
|
2540
|
+
github.com/swaggo/files v1.0.1 // indirect
|
|
2541
|
+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
2542
|
+
github.com/ugorji/go/codec v1.2.12 // indirect
|
|
2543
|
+
golang.org/x/arch v0.8.0 // indirect
|
|
2544
|
+
golang.org/x/crypto v0.23.0 // indirect
|
|
2545
|
+
golang.org/x/net v0.25.0 // indirect
|
|
2546
|
+
golang.org/x/sys v0.20.0 // indirect
|
|
2547
|
+
golang.org/x/text v0.15.0 // indirect
|
|
2548
|
+
golang.org/x/tools v0.21.0 // indirect
|
|
2549
|
+
google.golang.org/protobuf v1.34.1 // indirect
|
|
2550
|
+
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
2551
|
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
2552
|
+
)
|
|
2553
|
+
`}function Jr(e){return `package config
|
|
2554
|
+
|
|
2555
|
+
import (
|
|
2556
|
+
"log/slog"
|
|
2557
|
+
"os"
|
|
2558
|
+
"strings"
|
|
2559
|
+
)
|
|
2560
|
+
|
|
2561
|
+
// Config holds application configuration loaded from environment variables.
|
|
2562
|
+
type Config struct {
|
|
2563
|
+
Port string
|
|
2564
|
+
Env string
|
|
2565
|
+
GinMode string
|
|
2566
|
+
LogLevel string
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// Load reads configuration from environment variables with sensible defaults.
|
|
2570
|
+
func Load() *Config {
|
|
2571
|
+
env := getEnv("APP_ENV", "development")
|
|
2572
|
+
return &Config{
|
|
2573
|
+
Port: getEnv("PORT", "${e.port}"),
|
|
2574
|
+
Env: env,
|
|
2575
|
+
GinMode: getEnv("GIN_MODE", "debug"),
|
|
2576
|
+
LogLevel: getEnv("LOG_LEVEL", defaultLogLevel(env)),
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
// ParseLogLevel maps a level string to the corresponding slog.Level.
|
|
2581
|
+
// Falls back to Info for unrecognised values.
|
|
2582
|
+
func ParseLogLevel(s string) slog.Level {
|
|
2583
|
+
switch strings.ToLower(s) {
|
|
2584
|
+
case "debug":
|
|
2585
|
+
return slog.LevelDebug
|
|
2586
|
+
case "warn", "warning":
|
|
2587
|
+
return slog.LevelWarn
|
|
2588
|
+
case "error":
|
|
2589
|
+
return slog.LevelError
|
|
2590
|
+
default:
|
|
2591
|
+
return slog.LevelInfo
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
func defaultLogLevel(env string) string {
|
|
2596
|
+
if env == "development" {
|
|
2597
|
+
return "debug"
|
|
2598
|
+
}
|
|
2599
|
+
return "info"
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
func getEnv(key, fallback string) string {
|
|
2603
|
+
if v, ok := os.LookupEnv(key); ok && v != "" {
|
|
2604
|
+
return v
|
|
2605
|
+
}
|
|
2606
|
+
return fallback
|
|
2607
|
+
}
|
|
2608
|
+
`}function zr(e){return `package server
|
|
2609
|
+
|
|
2610
|
+
import (
|
|
2611
|
+
"net/http"
|
|
2612
|
+
|
|
2613
|
+
"github.com/gin-gonic/gin"
|
|
2614
|
+
ginSwagger "github.com/swaggo/gin-swagger"
|
|
2615
|
+
swaggerFiles "github.com/swaggo/files"
|
|
2616
|
+
|
|
2617
|
+
"${e.module_path}/internal/apierr"
|
|
2618
|
+
"${e.module_path}/internal/config"
|
|
2619
|
+
"${e.module_path}/internal/handlers"
|
|
2620
|
+
"${e.module_path}/internal/middleware"
|
|
2621
|
+
)
|
|
2622
|
+
|
|
2623
|
+
// NewRouter assembles the Gin engine with all middleware and routes.
|
|
2624
|
+
// Call this from main \u2014 or from tests via server.NewRouter(cfg).
|
|
2625
|
+
func NewRouter(cfg *config.Config) *gin.Engine {
|
|
2626
|
+
if cfg.GinMode == "release" {
|
|
2627
|
+
gin.SetMode(gin.ReleaseMode)
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
r := gin.New()
|
|
2631
|
+
r.Use(gin.Recovery())
|
|
2632
|
+
r.Use(middleware.CORS())
|
|
2633
|
+
r.Use(middleware.RequestID())
|
|
2634
|
+
r.Use(middleware.RateLimit())
|
|
2635
|
+
r.Use(middleware.Logger())
|
|
2636
|
+
|
|
2637
|
+
// Swagger UI \u2014 /docs redirects to /docs/index.html
|
|
2638
|
+
r.GET("/docs", func(c *gin.Context) { c.Redirect(http.StatusFound, "/docs/index.html") })
|
|
2639
|
+
r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
|
2640
|
+
|
|
2641
|
+
v1 := r.Group("/api/v1")
|
|
2642
|
+
{
|
|
2643
|
+
v1.GET("/health/live", handlers.Liveness)
|
|
2644
|
+
v1.GET("/health/ready", handlers.Readiness)
|
|
2645
|
+
v1.GET("/echo/:name", handlers.EchoParams)
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// Return JSON for unknown routes/methods instead of Gin's default text responses.
|
|
2649
|
+
// HandleMethodNotAllowed must be true so NoMethod handler fires for 405 cases.
|
|
2650
|
+
r.HandleMethodNotAllowed = true
|
|
2651
|
+
r.NoRoute(func(c *gin.Context) {
|
|
2652
|
+
apierr.NotFound(c, "route not found")
|
|
2653
|
+
})
|
|
2654
|
+
r.NoMethod(func(c *gin.Context) {
|
|
2655
|
+
apierr.MethodNotAllowed(c)
|
|
2656
|
+
})
|
|
2657
|
+
|
|
2658
|
+
return r
|
|
2659
|
+
}
|
|
2660
|
+
`}function Yr(){return `package handlers
|
|
2661
|
+
|
|
2662
|
+
import (
|
|
2663
|
+
"net/http"
|
|
2664
|
+
"time"
|
|
2665
|
+
|
|
2666
|
+
"github.com/gin-gonic/gin"
|
|
2667
|
+
)
|
|
2668
|
+
|
|
2669
|
+
// Liveness signals the process is alive (Kubernetes livenessProbe).
|
|
2670
|
+
//
|
|
2671
|
+
// @Summary Liveness probe
|
|
2672
|
+
// @Description Returns 200 when the process is alive.
|
|
2673
|
+
// @Tags health
|
|
2674
|
+
// @Produce json
|
|
2675
|
+
// @Success 200 {object} map[string]string
|
|
2676
|
+
// @Router /api/v1/health/live [get]
|
|
2677
|
+
func Liveness(c *gin.Context) {
|
|
2678
|
+
c.JSON(http.StatusOK, gin.H{
|
|
2679
|
+
"status": "ok",
|
|
2680
|
+
"time": time.Now().UTC().Format(time.RFC3339),
|
|
2681
|
+
})
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
// Readiness signals the service can accept traffic (Kubernetes readinessProbe).
|
|
2685
|
+
// Extend this function to check database connectivity, caches, etc.
|
|
2686
|
+
//
|
|
2687
|
+
// @Summary Readiness probe
|
|
2688
|
+
// @Description Returns 200 when the service is ready to accept traffic.
|
|
2689
|
+
// @Tags health
|
|
2690
|
+
// @Produce json
|
|
2691
|
+
// @Success 200 {object} map[string]string
|
|
2692
|
+
// @Router /api/v1/health/ready [get]
|
|
2693
|
+
func Readiness(c *gin.Context) {
|
|
2694
|
+
c.JSON(http.StatusOK, gin.H{
|
|
2695
|
+
"status": "ready",
|
|
2696
|
+
"time": time.Now().UTC().Format(time.RFC3339),
|
|
2697
|
+
})
|
|
2698
|
+
}
|
|
2699
|
+
`}function Qr(e){return `package handlers_test
|
|
2700
|
+
|
|
2701
|
+
import (
|
|
2702
|
+
"encoding/json"
|
|
2703
|
+
"net/http"
|
|
2704
|
+
"net/http/httptest"
|
|
2705
|
+
"testing"
|
|
2706
|
+
|
|
2707
|
+
"github.com/gin-gonic/gin"
|
|
2708
|
+
|
|
2709
|
+
"${e.module_path}/internal/config"
|
|
2710
|
+
"${e.module_path}/internal/server"
|
|
2711
|
+
)
|
|
2712
|
+
|
|
2713
|
+
func init() { gin.SetMode(gin.TestMode) }
|
|
2714
|
+
|
|
2715
|
+
func newRouter() *gin.Engine { return server.NewRouter(config.Load()) }
|
|
2716
|
+
|
|
2717
|
+
func TestLiveness(t *testing.T) {
|
|
2718
|
+
w := httptest.NewRecorder()
|
|
2719
|
+
req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
|
|
2720
|
+
newRouter().ServeHTTP(w, req)
|
|
2721
|
+
|
|
2722
|
+
if w.Code != http.StatusOK {
|
|
2723
|
+
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
var body map[string]any
|
|
2727
|
+
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
2728
|
+
t.Fatalf("invalid JSON: %v", err)
|
|
2729
|
+
}
|
|
2730
|
+
if body["status"] != "ok" {
|
|
2731
|
+
t.Fatalf("expected ok, got %v", body["status"])
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
func TestReadiness(t *testing.T) {
|
|
2736
|
+
w := httptest.NewRecorder()
|
|
2737
|
+
req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/ready", nil)
|
|
2738
|
+
newRouter().ServeHTTP(w, req)
|
|
2739
|
+
|
|
2740
|
+
if w.Code != http.StatusOK {
|
|
2741
|
+
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
`}function Xr(){return `# \u2500\u2500 Build stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2745
|
+
FROM golang:1.24-alpine AS builder
|
|
2746
|
+
|
|
2747
|
+
# Build-time version injection
|
|
2748
|
+
ARG VERSION=dev
|
|
2749
|
+
ARG COMMIT=none
|
|
2750
|
+
ARG DATE=unknown
|
|
2751
|
+
|
|
2752
|
+
WORKDIR /app
|
|
2753
|
+
COPY go.mod go.sum ./
|
|
2754
|
+
RUN go mod download
|
|
2755
|
+
|
|
2756
|
+
COPY . .
|
|
2757
|
+
RUN CGO_ENABLED=0 GOOS=linux go build \\
|
|
2758
|
+
-ldflags="-s -w -X main.version=$\${VERSION} -X main.commit=$\${COMMIT} -X main.date=$\${DATE}" \\
|
|
2759
|
+
-o server ./cmd/server
|
|
2760
|
+
|
|
2761
|
+
# \u2500\u2500 Runtime stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2762
|
+
# alpine includes busybox wget required for the HEALTHCHECK below.
|
|
2763
|
+
FROM alpine:3.21
|
|
2764
|
+
|
|
2765
|
+
RUN addgroup -S app && adduser -S -G app app
|
|
2766
|
+
COPY --from=builder /app/server /server
|
|
2767
|
+
USER app
|
|
2768
|
+
|
|
2769
|
+
EXPOSE 8080
|
|
2770
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
|
|
2771
|
+
CMD wget -qO- http://localhost:8080/api/v1/health/live || exit 1
|
|
2772
|
+
ENTRYPOINT ["/server"]
|
|
2773
|
+
`}function Zr(e){return `version: "3.9"
|
|
2774
|
+
|
|
2775
|
+
services:
|
|
2776
|
+
api:
|
|
2777
|
+
build: .
|
|
2778
|
+
container_name: ${e.project_name}
|
|
2779
|
+
ports:
|
|
2780
|
+
- "${e.port}:${e.port}"
|
|
2781
|
+
environment:
|
|
2782
|
+
PORT: "${e.port}"
|
|
2783
|
+
APP_ENV: development
|
|
2784
|
+
GIN_MODE: debug
|
|
2785
|
+
LOG_LEVEL: info
|
|
2786
|
+
CORS_ALLOW_ORIGINS: "*"
|
|
2787
|
+
RATE_LIMIT_RPS: "100"
|
|
2788
|
+
restart: unless-stopped
|
|
2789
|
+
`}function en(e){return `.PHONY: dev run build test cover lint fmt tidy docs docker-up docker-down
|
|
2790
|
+
|
|
2791
|
+
# Build-time metadata
|
|
2792
|
+
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
|
2793
|
+
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
|
|
2794
|
+
DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
2795
|
+
LDFLAGS = -ldflags "-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)"
|
|
2796
|
+
# Go tool binaries are installed to GOPATH/bin; include it so \`air\` and \`swag\` are found.
|
|
2797
|
+
GOBIN ?= $(shell go env GOPATH)/bin
|
|
2798
|
+
|
|
2799
|
+
# Hot reload \u2014 installs air on first use
|
|
2800
|
+
dev:
|
|
2801
|
+
@test -x "$(GOBIN)/air" || go install github.com/air-verse/air@latest
|
|
2802
|
+
GIN_MODE=debug $(GOBIN)/air
|
|
2803
|
+
|
|
2804
|
+
run:
|
|
2805
|
+
GIN_MODE=debug go run $(LDFLAGS) ./cmd/server
|
|
2806
|
+
|
|
2807
|
+
build:
|
|
2808
|
+
go build $(LDFLAGS) -o bin/${e.project_name} ./cmd/server
|
|
2809
|
+
|
|
2810
|
+
test:
|
|
2811
|
+
GIN_MODE=test go test ./... -v -race
|
|
2812
|
+
|
|
2813
|
+
cover:
|
|
2814
|
+
GIN_MODE=test go test ./... -race -coverprofile=coverage.out
|
|
2815
|
+
go tool cover -html=coverage.out -o coverage.html
|
|
2816
|
+
@echo "Coverage report: coverage.html"
|
|
2817
|
+
|
|
2818
|
+
# Generate Swagger docs \u2014 installs swag on first use
|
|
2819
|
+
docs:
|
|
2820
|
+
@test -x "$(GOBIN)/swag" || go install github.com/swaggo/swag/cmd/swag@latest
|
|
2821
|
+
$(GOBIN)/swag init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency
|
|
2822
|
+
|
|
2823
|
+
lint:
|
|
2824
|
+
@command -v golangci-lint >/dev/null 2>&1 || (echo "golangci-lint not found. Install: https://golangci-lint.run/usage/install/" && exit 1)
|
|
2825
|
+
golangci-lint run ./...
|
|
2826
|
+
|
|
2827
|
+
fmt:
|
|
2828
|
+
gofmt -w .
|
|
2829
|
+
|
|
2830
|
+
tidy:
|
|
2831
|
+
go mod tidy
|
|
2832
|
+
|
|
2833
|
+
docker-up:
|
|
2834
|
+
go mod tidy
|
|
2835
|
+
docker compose up --build \\
|
|
2836
|
+
--build-arg VERSION=$(VERSION) \\
|
|
2837
|
+
--build-arg COMMIT=$(COMMIT) \\
|
|
2838
|
+
--build-arg DATE=$(DATE) \\
|
|
2839
|
+
-d
|
|
2840
|
+
|
|
2841
|
+
docker-down:
|
|
2842
|
+
docker compose down
|
|
2843
|
+
`}function tn(e){return `# Application
|
|
2844
|
+
PORT=${e.port}
|
|
2845
|
+
APP_ENV=development
|
|
2846
|
+
GIN_MODE=debug
|
|
2847
|
+
LOG_LEVEL=debug
|
|
2848
|
+
|
|
2849
|
+
# CORS \u2014 comma-separated list of allowed origins, or * to allow all
|
|
2850
|
+
CORS_ALLOW_ORIGINS=*
|
|
2851
|
+
|
|
2852
|
+
# Rate limiting \u2014 max requests per IP per second
|
|
2853
|
+
RATE_LIMIT_RPS=100
|
|
2854
|
+
`}function on(){return `# Binaries
|
|
2855
|
+
bin/
|
|
2856
|
+
*.exe
|
|
2857
|
+
*.exe~
|
|
2858
|
+
*.dll
|
|
2859
|
+
*.so
|
|
2860
|
+
*.dylib
|
|
2861
|
+
|
|
2862
|
+
# Test binary
|
|
2863
|
+
*.test
|
|
2864
|
+
|
|
2865
|
+
# Output of go coverage tool
|
|
2866
|
+
*.out
|
|
2867
|
+
coverage.html
|
|
2868
|
+
|
|
2869
|
+
# Go workspace
|
|
2870
|
+
go.work
|
|
2871
|
+
go.work.sum
|
|
2872
|
+
|
|
2873
|
+
# Environment
|
|
2874
|
+
.env
|
|
2875
|
+
.env.local
|
|
2876
|
+
|
|
2877
|
+
# Hot reload (air)
|
|
2878
|
+
tmp/
|
|
2879
|
+
|
|
2880
|
+
# Swagger \u2014 generated files (committed stub docs/doc.go; run \`make docs\` to regenerate)
|
|
2881
|
+
docs/swagger.json
|
|
2882
|
+
docs/swagger.yaml
|
|
2883
|
+
docs/docs.go
|
|
2884
|
+
|
|
2885
|
+
# Editor
|
|
2886
|
+
.idea/
|
|
2887
|
+
.vscode/
|
|
2888
|
+
*.swp
|
|
2889
|
+
*.swo
|
|
2890
|
+
|
|
2891
|
+
# OS
|
|
2892
|
+
.DS_Store
|
|
2893
|
+
Thumbs.db
|
|
2894
|
+
`}function rn(e){return `name: CI
|
|
2895
|
+
|
|
2896
|
+
on:
|
|
2897
|
+
push:
|
|
2898
|
+
branches: [main, develop]
|
|
2899
|
+
pull_request:
|
|
2900
|
+
branches: [main]
|
|
2901
|
+
|
|
2902
|
+
jobs:
|
|
2903
|
+
test:
|
|
2904
|
+
name: Test
|
|
2905
|
+
runs-on: ubuntu-latest
|
|
2906
|
+
|
|
2907
|
+
steps:
|
|
2908
|
+
- uses: actions/checkout@v4
|
|
2909
|
+
|
|
2910
|
+
- name: Set up Go
|
|
2911
|
+
uses: actions/setup-go@v5
|
|
2912
|
+
with:
|
|
2913
|
+
go-version: "${e.go_version}"
|
|
2914
|
+
cache: true
|
|
2915
|
+
|
|
2916
|
+
- name: Tidy
|
|
2917
|
+
run: go mod tidy
|
|
2918
|
+
|
|
2919
|
+
- name: Build
|
|
2920
|
+
run: go build ./...
|
|
2921
|
+
|
|
2922
|
+
- name: Test
|
|
2923
|
+
run: GIN_MODE=test go test ./... -race -coverprofile=coverage.out
|
|
2924
|
+
|
|
2925
|
+
- name: Upload coverage
|
|
2926
|
+
uses: actions/upload-artifact@v4
|
|
2927
|
+
with:
|
|
2928
|
+
name: coverage
|
|
2929
|
+
path: coverage.out
|
|
2930
|
+
|
|
2931
|
+
lint:
|
|
2932
|
+
name: Lint
|
|
2933
|
+
runs-on: ubuntu-latest
|
|
2934
|
+
|
|
2935
|
+
steps:
|
|
2936
|
+
- uses: actions/checkout@v4
|
|
2937
|
+
|
|
2938
|
+
- name: Set up Go
|
|
2939
|
+
uses: actions/setup-go@v5
|
|
2940
|
+
with:
|
|
2941
|
+
go-version: "${e.go_version}"
|
|
2942
|
+
cache: true
|
|
2943
|
+
|
|
2944
|
+
- name: golangci-lint
|
|
2945
|
+
uses: golangci/golangci-lint-action@v6
|
|
2946
|
+
with:
|
|
2947
|
+
version: latest
|
|
2948
|
+
`}function nn(e){return `# ${Jt(e.project_name)}
|
|
2949
|
+
|
|
2950
|
+
> ${e.description}
|
|
2951
|
+
|
|
2952
|
+
Built with [Go](https://go.dev/) + [Gin](https://gin-gonic.com/) \xB7 Scaffolded by [RapidKit](https://getrapidkit.com)
|
|
2953
|
+
|
|
2954
|
+
## Quick start
|
|
2955
|
+
|
|
2956
|
+
\`\`\`bash
|
|
2957
|
+
# Run locally (hot reload)
|
|
2958
|
+
make dev
|
|
2959
|
+
|
|
2960
|
+
# Run tests
|
|
2961
|
+
make test
|
|
2962
|
+
|
|
2963
|
+
# Build binary
|
|
2964
|
+
make build
|
|
2965
|
+
|
|
2966
|
+
# Generate / refresh Swagger docs
|
|
2967
|
+
make docs
|
|
2968
|
+
|
|
2969
|
+
# Docker
|
|
2970
|
+
make docker-up
|
|
2971
|
+
\`\`\`
|
|
2972
|
+
|
|
2973
|
+
## Swagger / OpenAPI
|
|
2974
|
+
|
|
2975
|
+
After running \`make docs\`, the interactive UI is available at:
|
|
2976
|
+
|
|
2977
|
+
\`\`\`
|
|
2978
|
+
http://localhost:${e.port}/docs
|
|
2979
|
+
\`\`\`
|
|
2980
|
+
|
|
2981
|
+
The raw OpenAPI spec is served at \`/docs/doc.json\`.
|
|
2982
|
+
|
|
2983
|
+
## Endpoints
|
|
2984
|
+
|
|
2985
|
+
| Method | Path | Description |
|
|
2986
|
+
|--------|------|--------------|
|
|
2987
|
+
| GET | /api/v1/health/live | Kubernetes livenessProbe |
|
|
2988
|
+
| GET | /api/v1/health/ready | Kubernetes readinessProbe |
|
|
2989
|
+
| GET | /api/v1/echo/:name | Example handler \u2014 remove in production |
|
|
2990
|
+
| GET | /docs/* | Swagger UI (OpenAPI docs) |
|
|
2991
|
+
|
|
2992
|
+
## Configuration
|
|
2993
|
+
|
|
2994
|
+
All configuration is done through environment variables (see \`.env.example\`):
|
|
2995
|
+
|
|
2996
|
+
| Variable | Default | Description |
|
|
2997
|
+
|----------|---------|-------------|
|
|
2998
|
+
| \`PORT\` | \`${e.port}\` | HTTP listen port |
|
|
2999
|
+
| \`APP_ENV\` | \`development\` | Application environment |
|
|
3000
|
+
| \`GIN_MODE\` | \`debug\` | \`debug\` \\| \`release\` \\| \`test\` |
|
|
3001
|
+
| \`LOG_LEVEL\` | \`debug\` / \`info\` | \`debug\` \\| \`info\` \\| \`warn\` \\| \`error\` |
|
|
3002
|
+
| \`CORS_ALLOW_ORIGINS\` | \`*\` | Comma-separated list of allowed origins, or \`*\` |
|
|
3003
|
+
| \`RATE_LIMIT_RPS\` | \`100\` | Max requests per IP per second |
|
|
3004
|
+
|
|
3005
|
+
## Project structure
|
|
3006
|
+
|
|
3007
|
+
\`\`\`
|
|
3008
|
+
${e.project_name}/
|
|
3009
|
+
\u251C\u2500\u2500 cmd/
|
|
3010
|
+
\u2502 \u2514\u2500\u2500 server/
|
|
3011
|
+
\u2502 \u2514\u2500\u2500 main.go # Graceful shutdown + version ldflags
|
|
3012
|
+
\u251C\u2500\u2500 docs/ # Swagger generated files (\`make docs\`)
|
|
3013
|
+
\u2502 \u2514\u2500\u2500 doc.go # Package-level OpenAPI annotations
|
|
3014
|
+
\u251C\u2500\u2500 internal/
|
|
3015
|
+
\u2502 \u251C\u2500\u2500 apierr/ # Consistent JSON error envelope
|
|
3016
|
+
\u2502 \u2502 \u251C\u2500\u2500 apierr.go
|
|
3017
|
+
\u2502 \u2502 \u2514\u2500\u2500 apierr_test.go
|
|
3018
|
+
\u2502 \u251C\u2500\u2500 config/ # 12-factor configuration
|
|
3019
|
+
\u2502 \u2502 \u251C\u2500\u2500 config.go
|
|
3020
|
+
\u2502 \u2502 \u2514\u2500\u2500 config_test.go
|
|
3021
|
+
\u2502 \u251C\u2500\u2500 handlers/ # HTTP handlers + tests
|
|
3022
|
+
\u2502 \u2502 \u251C\u2500\u2500 health.go
|
|
3023
|
+
\u2502 \u2502 \u251C\u2500\u2500 health_test.go
|
|
3024
|
+
\u2502 \u2502 \u251C\u2500\u2500 example.go # EchoParams \u2014 replace with your own handlers
|
|
3025
|
+
\u2502 \u2502 \u2514\u2500\u2500 example_test.go
|
|
3026
|
+
\u2502 \u251C\u2500\u2500 middleware/
|
|
3027
|
+
\u2502 \u2502 \u251C\u2500\u2500 requestid.go # X-Request-ID + structured logger
|
|
3028
|
+
\u2502 \u2502 \u251C\u2500\u2500 requestid_test.go
|
|
3029
|
+
\u2502 \u2502 \u251C\u2500\u2500 cors.go # CORS (CORS_ALLOW_ORIGINS)
|
|
3030
|
+
\u2502 \u2502 \u251C\u2500\u2500 cors_test.go
|
|
3031
|
+
\u2502 \u2502 \u251C\u2500\u2500 ratelimit.go # Per-IP limiter (RATE_LIMIT_RPS)
|
|
3032
|
+
\u2502 \u2502 \u2514\u2500\u2500 ratelimit_test.go
|
|
3033
|
+
\u2502 \u2514\u2500\u2500 server/
|
|
3034
|
+
\u2502 \u251C\u2500\u2500 server.go
|
|
3035
|
+
\u2502 \u2514\u2500\u2500 server_test.go
|
|
3036
|
+
\u251C\u2500\u2500 .air.toml # Hot reload
|
|
3037
|
+
\u251C\u2500\u2500 .github/workflows/ci.yml # CI: test + lint
|
|
3038
|
+
\u251C\u2500\u2500 .golangci.yml
|
|
3039
|
+
\u251C\u2500\u2500 Dockerfile # Multi-stage, alpine HEALTHCHECK
|
|
3040
|
+
\u251C\u2500\u2500 docker-compose.yml
|
|
3041
|
+
\u251C\u2500\u2500 Makefile
|
|
3042
|
+
\u2514\u2500\u2500 README.md
|
|
3043
|
+
\`\`\`
|
|
3044
|
+
|
|
3045
|
+
## Available commands
|
|
3046
|
+
|
|
3047
|
+
| Command | Description |
|
|
3048
|
+
|---------|-------------|
|
|
3049
|
+
| \`make dev\` | Hot reload via [air](https://github.com/air-verse/air) |
|
|
3050
|
+
| \`make run\` | Run without hot reload |
|
|
3051
|
+
| \`make build\` | Binary with version ldflags |
|
|
3052
|
+
| \`make test\` | Run tests with race detector |
|
|
3053
|
+
| \`make cover\` | HTML coverage report |
|
|
3054
|
+
| \`make docs\` | Re-generate Swagger JSON (needs \`swag\`) |
|
|
3055
|
+
| \`make lint\` | golangci-lint |
|
|
3056
|
+
| \`make fmt\` | gofmt |
|
|
3057
|
+
| \`make tidy\` | go mod tidy |
|
|
3058
|
+
| \`make docker-up\` | Build & run via Docker Compose |
|
|
3059
|
+
| \`make docker-down\` | Stop |
|
|
3060
|
+
|
|
3061
|
+
## License
|
|
3062
|
+
|
|
3063
|
+
${e.app_version} \xB7 ${e.author}
|
|
3064
|
+
`}function sn(){return `package middleware
|
|
3065
|
+
|
|
3066
|
+
import (
|
|
3067
|
+
"crypto/rand"
|
|
3068
|
+
"encoding/hex"
|
|
3069
|
+
"log/slog"
|
|
3070
|
+
"time"
|
|
3071
|
+
|
|
3072
|
+
"github.com/gin-gonic/gin"
|
|
3073
|
+
)
|
|
3074
|
+
|
|
3075
|
+
const headerRequestID = "X-Request-ID"
|
|
3076
|
+
|
|
3077
|
+
// RequestID injects a unique identifier into every request.
|
|
3078
|
+
// If the caller sends an X-Request-ID header it is reused; otherwise a new one
|
|
3079
|
+
// is generated and written back in the response.
|
|
3080
|
+
func RequestID() gin.HandlerFunc {
|
|
3081
|
+
return func(c *gin.Context) {
|
|
3082
|
+
id := c.GetHeader(headerRequestID)
|
|
3083
|
+
if id == "" {
|
|
3084
|
+
id = newID()
|
|
3085
|
+
}
|
|
3086
|
+
c.Set(headerRequestID, id)
|
|
3087
|
+
c.Header(headerRequestID, id)
|
|
3088
|
+
c.Next()
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
// Logger emits a structured JSON log line after each request.
|
|
3093
|
+
func Logger() gin.HandlerFunc {
|
|
3094
|
+
return func(c *gin.Context) {
|
|
3095
|
+
start := time.Now()
|
|
3096
|
+
c.Next()
|
|
3097
|
+
slog.Info("http",
|
|
3098
|
+
"method", c.Request.Method,
|
|
3099
|
+
"path", c.Request.URL.Path,
|
|
3100
|
+
"status", c.Writer.Status(),
|
|
3101
|
+
"bytes", c.Writer.Size(),
|
|
3102
|
+
"latency_ms", time.Since(start).Milliseconds(),
|
|
3103
|
+
"ip", c.ClientIP(),
|
|
3104
|
+
"request_id", c.GetString(headerRequestID),
|
|
3105
|
+
)
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
func newID() string {
|
|
3110
|
+
b := make([]byte, 8)
|
|
3111
|
+
if _, err := rand.Read(b); err != nil {
|
|
3112
|
+
return "unknown"
|
|
3113
|
+
}
|
|
3114
|
+
return hex.EncodeToString(b)
|
|
3115
|
+
}
|
|
3116
|
+
`}function an(e){return `package middleware_test
|
|
3117
|
+
|
|
3118
|
+
import (
|
|
3119
|
+
"net/http"
|
|
3120
|
+
"net/http/httptest"
|
|
3121
|
+
"testing"
|
|
3122
|
+
|
|
3123
|
+
"github.com/gin-gonic/gin"
|
|
3124
|
+
|
|
3125
|
+
"${e.module_path}/internal/middleware"
|
|
3126
|
+
)
|
|
3127
|
+
|
|
3128
|
+
func init() { gin.SetMode(gin.TestMode) }
|
|
3129
|
+
|
|
3130
|
+
func newTestRouter() *gin.Engine {
|
|
3131
|
+
r := gin.New()
|
|
3132
|
+
r.Use(middleware.RequestID())
|
|
3133
|
+
r.Use(middleware.Logger())
|
|
3134
|
+
r.GET("/ping", func(c *gin.Context) {
|
|
3135
|
+
c.String(http.StatusOK, "pong")
|
|
3136
|
+
})
|
|
3137
|
+
return r
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
func TestRequestID_IsGenerated(t *testing.T) {
|
|
3141
|
+
w := httptest.NewRecorder()
|
|
3142
|
+
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
|
3143
|
+
newTestRouter().ServeHTTP(w, req)
|
|
3144
|
+
|
|
3145
|
+
id := w.Header().Get("X-Request-ID")
|
|
3146
|
+
if id == "" {
|
|
3147
|
+
t.Fatal("expected X-Request-ID header to be set")
|
|
3148
|
+
}
|
|
3149
|
+
if len(id) != 16 { // 8 random bytes \u2192 16 hex chars
|
|
3150
|
+
t.Fatalf("unexpected request ID length %d, want 16", len(id))
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
func TestRequestID_IsReused(t *testing.T) {
|
|
3155
|
+
w := httptest.NewRecorder()
|
|
3156
|
+
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
|
3157
|
+
req.Header.Set("X-Request-ID", "my-trace-id")
|
|
3158
|
+
newTestRouter().ServeHTTP(w, req)
|
|
3159
|
+
|
|
3160
|
+
id := w.Header().Get("X-Request-ID")
|
|
3161
|
+
if id != "my-trace-id" {
|
|
3162
|
+
t.Fatalf("expected X-Request-ID to be reused, got %q", id)
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
`}function cn(){return `// Package apierr provides a consistent JSON error envelope for all API responses.
|
|
3166
|
+
//
|
|
3167
|
+
// Every error response looks like:
|
|
3168
|
+
//
|
|
3169
|
+
// {"error": "user not found", "code": "NOT_FOUND", "request_id": "a1b2c3d4..."}
|
|
3170
|
+
package apierr
|
|
3171
|
+
|
|
3172
|
+
import (
|
|
3173
|
+
"net/http"
|
|
3174
|
+
|
|
3175
|
+
"github.com/gin-gonic/gin"
|
|
3176
|
+
)
|
|
3177
|
+
|
|
3178
|
+
// Response is the standard error envelope returned by all API endpoints.
|
|
3179
|
+
type Response struct {
|
|
3180
|
+
Error string \`json:"error"\`
|
|
3181
|
+
Code string \`json:"code"\`
|
|
3182
|
+
RequestID string \`json:"request_id,omitempty"\`
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
func reply(c *gin.Context, status int, msg, code string) {
|
|
3186
|
+
c.AbortWithStatusJSON(status, Response{
|
|
3187
|
+
Error: msg,
|
|
3188
|
+
Code: code,
|
|
3189
|
+
RequestID: c.GetString("X-Request-ID"),
|
|
3190
|
+
})
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
// BadRequest responds with 400 and code "BAD_REQUEST".
|
|
3194
|
+
func BadRequest(c *gin.Context, msg string) {
|
|
3195
|
+
reply(c, http.StatusBadRequest, msg, "BAD_REQUEST")
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
// NotFound responds with 404 and code "NOT_FOUND".
|
|
3199
|
+
func NotFound(c *gin.Context, msg string) {
|
|
3200
|
+
reply(c, http.StatusNotFound, msg, "NOT_FOUND")
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// Unauthorized responds with 401 and code "UNAUTHORIZED".
|
|
3204
|
+
func Unauthorized(c *gin.Context) {
|
|
3205
|
+
reply(c, http.StatusUnauthorized, "authentication required", "UNAUTHORIZED")
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
// Forbidden responds with 403 and code "FORBIDDEN".
|
|
3209
|
+
func Forbidden(c *gin.Context) {
|
|
3210
|
+
reply(c, http.StatusForbidden, "access denied", "FORBIDDEN")
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
// MethodNotAllowed responds with 405 and code "METHOD_NOT_ALLOWED".
|
|
3214
|
+
func MethodNotAllowed(c *gin.Context) {
|
|
3215
|
+
reply(c, http.StatusMethodNotAllowed, "method not allowed", "METHOD_NOT_ALLOWED")
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
// InternalError responds with 500 and code "INTERNAL_ERROR".
|
|
3219
|
+
// The original error is intentionally not exposed to the client.
|
|
3220
|
+
func InternalError(c *gin.Context, _ error) {
|
|
3221
|
+
reply(c, http.StatusInternalServerError, "an internal error occurred", "INTERNAL_ERROR")
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
// TooManyRequests responds with 429 and code "TOO_MANY_REQUESTS".
|
|
3225
|
+
func TooManyRequests(c *gin.Context) {
|
|
3226
|
+
reply(c, http.StatusTooManyRequests, "rate limit exceeded", "TOO_MANY_REQUESTS")
|
|
3227
|
+
}
|
|
3228
|
+
`}function dn(e){return `package apierr_test
|
|
3229
|
+
|
|
3230
|
+
import (
|
|
3231
|
+
"encoding/json"
|
|
3232
|
+
"net/http"
|
|
3233
|
+
"net/http/httptest"
|
|
3234
|
+
"testing"
|
|
3235
|
+
|
|
3236
|
+
"github.com/gin-gonic/gin"
|
|
3237
|
+
|
|
3238
|
+
"${e.module_path}/internal/apierr"
|
|
3239
|
+
)
|
|
3240
|
+
|
|
3241
|
+
func init() { gin.SetMode(gin.TestMode) }
|
|
3242
|
+
|
|
3243
|
+
func makeRouter(fn gin.HandlerFunc) *gin.Engine {
|
|
3244
|
+
r := gin.New()
|
|
3245
|
+
r.GET("/test", fn)
|
|
3246
|
+
return r
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
func readJSON(t *testing.T, w *httptest.ResponseRecorder) apierr.Response {
|
|
3250
|
+
t.Helper()
|
|
3251
|
+
var out apierr.Response
|
|
3252
|
+
if err := json.NewDecoder(w.Body).Decode(&out); err != nil {
|
|
3253
|
+
t.Fatalf("invalid JSON: %v", err)
|
|
3254
|
+
}
|
|
3255
|
+
return out
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
func TestBadRequest(t *testing.T) {
|
|
3259
|
+
w := httptest.NewRecorder()
|
|
3260
|
+
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
3261
|
+
makeRouter(func(c *gin.Context) { apierr.BadRequest(c, "invalid email") }).ServeHTTP(w, req)
|
|
3262
|
+
|
|
3263
|
+
if w.Code != http.StatusBadRequest {
|
|
3264
|
+
t.Fatalf("expected 400, got %d", w.Code)
|
|
3265
|
+
}
|
|
3266
|
+
body := readJSON(t, w)
|
|
3267
|
+
if body.Code != "BAD_REQUEST" {
|
|
3268
|
+
t.Fatalf("expected BAD_REQUEST, got %q", body.Code)
|
|
3269
|
+
}
|
|
3270
|
+
if body.Error != "invalid email" {
|
|
3271
|
+
t.Fatalf("unexpected error message: %q", body.Error)
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
func TestNotFound(t *testing.T) {
|
|
3276
|
+
w := httptest.NewRecorder()
|
|
3277
|
+
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
3278
|
+
makeRouter(func(c *gin.Context) { apierr.NotFound(c, "user not found") }).ServeHTTP(w, req)
|
|
3279
|
+
|
|
3280
|
+
if w.Code != http.StatusNotFound {
|
|
3281
|
+
t.Fatalf("expected 404, got %d", w.Code)
|
|
3282
|
+
}
|
|
3283
|
+
body := readJSON(t, w)
|
|
3284
|
+
if body.Code != "NOT_FOUND" {
|
|
3285
|
+
t.Fatalf("expected NOT_FOUND, got %q", body.Code)
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
func TestUnauthorized(t *testing.T) {
|
|
3290
|
+
w := httptest.NewRecorder()
|
|
3291
|
+
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
3292
|
+
makeRouter(func(c *gin.Context) { apierr.Unauthorized(c) }).ServeHTTP(w, req)
|
|
3293
|
+
|
|
3294
|
+
if w.Code != http.StatusUnauthorized {
|
|
3295
|
+
t.Fatalf("expected 401, got %d", w.Code)
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
func TestForbidden(t *testing.T) {
|
|
3300
|
+
w := httptest.NewRecorder()
|
|
3301
|
+
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
3302
|
+
makeRouter(func(c *gin.Context) { apierr.Forbidden(c) }).ServeHTTP(w, req)
|
|
3303
|
+
|
|
3304
|
+
if w.Code != http.StatusForbidden {
|
|
3305
|
+
t.Fatalf("expected 403, got %d", w.Code)
|
|
3306
|
+
}
|
|
3307
|
+
body := readJSON(t, w)
|
|
3308
|
+
if body.Code != "FORBIDDEN" {
|
|
3309
|
+
t.Fatalf("expected FORBIDDEN, got %q", body.Code)
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
func TestMethodNotAllowed(t *testing.T) {
|
|
3314
|
+
w := httptest.NewRecorder()
|
|
3315
|
+
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
3316
|
+
makeRouter(func(c *gin.Context) { apierr.MethodNotAllowed(c) }).ServeHTTP(w, req)
|
|
3317
|
+
|
|
3318
|
+
if w.Code != http.StatusMethodNotAllowed {
|
|
3319
|
+
t.Fatalf("expected 405, got %d", w.Code)
|
|
3320
|
+
}
|
|
3321
|
+
body := readJSON(t, w)
|
|
3322
|
+
if body.Code != "METHOD_NOT_ALLOWED" {
|
|
3323
|
+
t.Fatalf("expected METHOD_NOT_ALLOWED, got %q", body.Code)
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
func TestInternalError(t *testing.T) {
|
|
3328
|
+
w := httptest.NewRecorder()
|
|
3329
|
+
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
3330
|
+
makeRouter(func(c *gin.Context) { apierr.InternalError(c, nil) }).ServeHTTP(w, req)
|
|
3331
|
+
|
|
3332
|
+
if w.Code != http.StatusInternalServerError {
|
|
3333
|
+
t.Fatalf("expected 500, got %d", w.Code)
|
|
3334
|
+
}
|
|
3335
|
+
body := readJSON(t, w)
|
|
3336
|
+
if body.Code != "INTERNAL_ERROR" {
|
|
3337
|
+
t.Fatalf("expected INTERNAL_ERROR, got %q", body.Code)
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
func TestTooManyRequests(t *testing.T) {
|
|
3342
|
+
w := httptest.NewRecorder()
|
|
3343
|
+
req, _ := http.NewRequest(http.MethodGet, "/test", nil)
|
|
3344
|
+
makeRouter(func(c *gin.Context) { apierr.TooManyRequests(c) }).ServeHTTP(w, req)
|
|
3345
|
+
|
|
3346
|
+
if w.Code != http.StatusTooManyRequests {
|
|
3347
|
+
t.Fatalf("expected 429, got %d", w.Code)
|
|
3348
|
+
}
|
|
3349
|
+
body := readJSON(t, w)
|
|
3350
|
+
if body.Code != "TOO_MANY_REQUESTS" {
|
|
3351
|
+
t.Fatalf("expected TOO_MANY_REQUESTS, got %q", body.Code)
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
`}function ln(e){return `// Package docs provides the swaggo-generated OpenAPI specification.
|
|
3355
|
+
//
|
|
3356
|
+
// Run \`make docs\` to regenerate after changing handler annotations.
|
|
3357
|
+
//
|
|
3358
|
+
// @title ${Jt(e.project_name)} API
|
|
3359
|
+
// @version ${e.app_version}
|
|
3360
|
+
// @description ${e.description}
|
|
3361
|
+
// @host localhost:${e.port}
|
|
3362
|
+
// @BasePath /
|
|
3363
|
+
// @schemes http https
|
|
3364
|
+
//
|
|
3365
|
+
// @contact.name ${e.author}
|
|
3366
|
+
// @license.name MIT
|
|
3367
|
+
package docs
|
|
3368
|
+
`}function pn(e){return `package handlers
|
|
3369
|
+
|
|
3370
|
+
import (
|
|
3371
|
+
"net/http"
|
|
3372
|
+
|
|
3373
|
+
"github.com/gin-gonic/gin"
|
|
3374
|
+
|
|
3375
|
+
"${e.module_path}/internal/apierr"
|
|
3376
|
+
)
|
|
3377
|
+
|
|
3378
|
+
// EchoResponse is the JSON body returned by EchoParams.
|
|
3379
|
+
type EchoResponse struct {
|
|
3380
|
+
Name string \`json:"name"\`
|
|
3381
|
+
RequestID string \`json:"request_id"\`
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
// EchoParams is an example handler demonstrating how to:
|
|
3385
|
+
// - read URL path parameters
|
|
3386
|
+
// - use apierr for consistent JSON error responses
|
|
3387
|
+
// - access the request ID injected by RequestID middleware
|
|
3388
|
+
//
|
|
3389
|
+
// Replace or remove this file once you add your own business logic.
|
|
3390
|
+
//
|
|
3391
|
+
// @Summary Echo path parameter
|
|
3392
|
+
// @Description Returns the :name path parameter together with the request ID.
|
|
3393
|
+
// @Tags example
|
|
3394
|
+
// @Produce json
|
|
3395
|
+
// @Param name path string true "Name to echo"
|
|
3396
|
+
// @Success 200 {object} handlers.EchoResponse
|
|
3397
|
+
// @Failure 400 {object} apierr.Response
|
|
3398
|
+
// @Router /api/v1/echo/{name} [get]
|
|
3399
|
+
func EchoParams(c *gin.Context) {
|
|
3400
|
+
name := c.Param("name")
|
|
3401
|
+
if name == "" {
|
|
3402
|
+
apierr.BadRequest(c, "name parameter is required")
|
|
3403
|
+
return
|
|
3404
|
+
}
|
|
3405
|
+
c.JSON(http.StatusOK, EchoResponse{
|
|
3406
|
+
Name: name,
|
|
3407
|
+
RequestID: c.GetString("X-Request-ID"),
|
|
3408
|
+
})
|
|
3409
|
+
}
|
|
3410
|
+
`}function un(e){return `package handlers_test
|
|
3411
|
+
|
|
3412
|
+
import (
|
|
3413
|
+
"encoding/json"
|
|
3414
|
+
"net/http"
|
|
3415
|
+
"net/http/httptest"
|
|
3416
|
+
"testing"
|
|
3417
|
+
|
|
3418
|
+
"github.com/gin-gonic/gin"
|
|
3419
|
+
|
|
3420
|
+
"${e.module_path}/internal/handlers"
|
|
3421
|
+
"${e.module_path}/internal/middleware"
|
|
3422
|
+
)
|
|
3423
|
+
|
|
3424
|
+
func newEchoRouter() *gin.Engine {
|
|
3425
|
+
r := gin.New()
|
|
3426
|
+
r.Use(middleware.RequestID())
|
|
3427
|
+
r.GET("/echo/:name", handlers.EchoParams)
|
|
3428
|
+
return r
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
func TestEchoParams_Success(t *testing.T) {
|
|
3432
|
+
w := httptest.NewRecorder()
|
|
3433
|
+
req, _ := http.NewRequest(http.MethodGet, "/echo/alice", nil)
|
|
3434
|
+
newEchoRouter().ServeHTTP(w, req)
|
|
3435
|
+
|
|
3436
|
+
if w.Code != http.StatusOK {
|
|
3437
|
+
t.Fatalf("expected 200, got %d", w.Code)
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
var body map[string]any
|
|
3441
|
+
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
|
|
3442
|
+
t.Fatalf("invalid JSON: %v", err)
|
|
3443
|
+
}
|
|
3444
|
+
if body["name"] != "alice" {
|
|
3445
|
+
t.Fatalf("expected name=alice, got %v", body["name"])
|
|
3446
|
+
}
|
|
3447
|
+
if body["request_id"] == nil || body["request_id"] == "" {
|
|
3448
|
+
t.Fatal("expected request_id to be set by RequestID middleware")
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
// TestEchoParams_EmptyName registers EchoParams on a param-free route so that
|
|
3453
|
+
// c.Param("name") returns "" and the 400 guard executes.
|
|
3454
|
+
func TestEchoParams_EmptyName(t *testing.T) {
|
|
3455
|
+
gin.SetMode(gin.TestMode)
|
|
3456
|
+
r := gin.New()
|
|
3457
|
+
r.Use(middleware.RequestID())
|
|
3458
|
+
r.GET("/echo-bare", handlers.EchoParams)
|
|
3459
|
+
|
|
3460
|
+
w := httptest.NewRecorder()
|
|
3461
|
+
req, _ := http.NewRequest(http.MethodGet, "/echo-bare", nil)
|
|
3462
|
+
r.ServeHTTP(w, req)
|
|
3463
|
+
|
|
3464
|
+
if w.Code != http.StatusBadRequest {
|
|
3465
|
+
t.Fatalf("expected 400, got %d", w.Code)
|
|
3466
|
+
}
|
|
3467
|
+
var body map[string]any
|
|
3468
|
+
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
|
|
3469
|
+
t.Fatalf("invalid JSON: %v", err)
|
|
3470
|
+
}
|
|
3471
|
+
if body["code"] != "BAD_REQUEST" {
|
|
3472
|
+
t.Fatalf("expected code=BAD_REQUEST, got %v", body["code"])
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
`}function gn(e){return `package config_test
|
|
3476
|
+
|
|
3477
|
+
import (
|
|
3478
|
+
"log/slog"
|
|
3479
|
+
"testing"
|
|
3480
|
+
|
|
3481
|
+
"${e.module_path}/internal/config"
|
|
3482
|
+
)
|
|
3483
|
+
|
|
3484
|
+
func TestParseLogLevel(t *testing.T) {
|
|
3485
|
+
tests := []struct {
|
|
3486
|
+
input string
|
|
3487
|
+
want slog.Level
|
|
3488
|
+
}{
|
|
3489
|
+
{"debug", slog.LevelDebug},
|
|
3490
|
+
{"DEBUG", slog.LevelDebug},
|
|
3491
|
+
{"warn", slog.LevelWarn},
|
|
3492
|
+
{"warning", slog.LevelWarn},
|
|
3493
|
+
{"error", slog.LevelError},
|
|
3494
|
+
{"info", slog.LevelInfo},
|
|
3495
|
+
{"", slog.LevelInfo},
|
|
3496
|
+
{"unknown", slog.LevelInfo},
|
|
3497
|
+
}
|
|
3498
|
+
for _, tc := range tests {
|
|
3499
|
+
got := config.ParseLogLevel(tc.input)
|
|
3500
|
+
if got != tc.want {
|
|
3501
|
+
t.Errorf("ParseLogLevel(%q) = %v, want %v", tc.input, got, tc.want)
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
func TestLoad_EnvOverride(t *testing.T) {
|
|
3507
|
+
t.Setenv("PORT", "9090")
|
|
3508
|
+
t.Setenv("APP_ENV", "production")
|
|
3509
|
+
t.Setenv("LOG_LEVEL", "warn")
|
|
3510
|
+
t.Setenv("GIN_MODE", "release")
|
|
3511
|
+
|
|
3512
|
+
cfg := config.Load()
|
|
3513
|
+
|
|
3514
|
+
if cfg.Port != "9090" {
|
|
3515
|
+
t.Errorf("expected Port=9090, got %q", cfg.Port)
|
|
3516
|
+
}
|
|
3517
|
+
if cfg.Env != "production" {
|
|
3518
|
+
t.Errorf("expected Env=production, got %q", cfg.Env)
|
|
3519
|
+
}
|
|
3520
|
+
if cfg.LogLevel != "warn" {
|
|
3521
|
+
t.Errorf("expected LogLevel=warn, got %q", cfg.LogLevel)
|
|
3522
|
+
}
|
|
3523
|
+
if cfg.GinMode != "release" {
|
|
3524
|
+
t.Errorf("expected GinMode=release, got %q", cfg.GinMode)
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
func TestLoad_Defaults(t *testing.T) {
|
|
3529
|
+
// Empty string forces getEnv() to return the built-in fallback value.
|
|
3530
|
+
t.Setenv("PORT", "")
|
|
3531
|
+
t.Setenv("APP_ENV", "")
|
|
3532
|
+
t.Setenv("LOG_LEVEL", "")
|
|
3533
|
+
t.Setenv("GIN_MODE", "")
|
|
3534
|
+
|
|
3535
|
+
cfg := config.Load()
|
|
3536
|
+
|
|
3537
|
+
if cfg.Port != "${e.port}" {
|
|
3538
|
+
t.Errorf("expected default Port=${e.port}, got %q", cfg.Port)
|
|
3539
|
+
}
|
|
3540
|
+
if cfg.Env != "development" {
|
|
3541
|
+
t.Errorf("expected default Env=development, got %q", cfg.Env)
|
|
3542
|
+
}
|
|
3543
|
+
// APP_ENV="" \u2192 fallback "development" \u2192 defaultLogLevel \u2192 "debug"
|
|
3544
|
+
if cfg.LogLevel != "debug" {
|
|
3545
|
+
t.Errorf("expected default LogLevel=debug (development env), got %q", cfg.LogLevel)
|
|
3546
|
+
}
|
|
3547
|
+
if cfg.GinMode != "debug" {
|
|
3548
|
+
t.Errorf("expected default GinMode=debug, got %q", cfg.GinMode)
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
`}function mn(){return `package middleware
|
|
3552
|
+
|
|
3553
|
+
import (
|
|
3554
|
+
"net/http"
|
|
3555
|
+
"os"
|
|
3556
|
+
"strings"
|
|
3557
|
+
|
|
3558
|
+
"github.com/gin-gonic/gin"
|
|
3559
|
+
)
|
|
3560
|
+
|
|
3561
|
+
// CORS returns a Gin middleware configured via CORS_ALLOW_ORIGINS env var.
|
|
3562
|
+
//
|
|
3563
|
+
// Set CORS_ALLOW_ORIGINS="*" for development (the default when unset).
|
|
3564
|
+
// In production supply a comma-separated list of allowed origins:
|
|
3565
|
+
//
|
|
3566
|
+
// CORS_ALLOW_ORIGINS=https://app.example.com,https://admin.example.com
|
|
3567
|
+
func CORS() gin.HandlerFunc {
|
|
3568
|
+
allowed := os.Getenv("CORS_ALLOW_ORIGINS")
|
|
3569
|
+
if allowed == "" {
|
|
3570
|
+
allowed = "*"
|
|
3571
|
+
}
|
|
3572
|
+
allowAll := allowed == "*"
|
|
3573
|
+
|
|
3574
|
+
return func(c *gin.Context) {
|
|
3575
|
+
origin := c.Request.Header.Get("Origin")
|
|
3576
|
+
if allowAll {
|
|
3577
|
+
c.Header("Access-Control-Allow-Origin", "*")
|
|
3578
|
+
} else if origin != "" {
|
|
3579
|
+
for _, o := range strings.Split(allowed, ",") {
|
|
3580
|
+
if strings.TrimSpace(o) == origin {
|
|
3581
|
+
c.Header("Access-Control-Allow-Origin", origin)
|
|
3582
|
+
c.Header("Vary", "Origin")
|
|
3583
|
+
break
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
|
|
3588
|
+
c.Header("Access-Control-Allow-Headers", "Origin,Content-Type,Authorization,X-Request-ID")
|
|
3589
|
+
c.Header("Access-Control-Expose-Headers", "X-Request-ID")
|
|
3590
|
+
c.Header("Access-Control-Max-Age", "600")
|
|
3591
|
+
|
|
3592
|
+
if c.Request.Method == http.MethodOptions {
|
|
3593
|
+
c.AbortWithStatus(http.StatusNoContent)
|
|
3594
|
+
return
|
|
3595
|
+
}
|
|
3596
|
+
c.Next()
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
`}function fn(e){return `package middleware_test
|
|
3600
|
+
|
|
3601
|
+
import (
|
|
3602
|
+
"net/http"
|
|
3603
|
+
"net/http/httptest"
|
|
3604
|
+
"testing"
|
|
3605
|
+
|
|
3606
|
+
"github.com/gin-gonic/gin"
|
|
3607
|
+
|
|
3608
|
+
"${e.module_path}/internal/middleware"
|
|
3609
|
+
)
|
|
3610
|
+
|
|
3611
|
+
func newCORSRouter(t *testing.T) *gin.Engine {
|
|
3612
|
+
t.Helper()
|
|
3613
|
+
gin.SetMode(gin.TestMode)
|
|
3614
|
+
r := gin.New()
|
|
3615
|
+
r.Use(middleware.CORS())
|
|
3616
|
+
r.GET("/ping", func(c *gin.Context) { c.Status(http.StatusOK) })
|
|
3617
|
+
return r
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
func TestCORS_Wildcard(t *testing.T) {
|
|
3621
|
+
t.Setenv("CORS_ALLOW_ORIGINS", "*")
|
|
3622
|
+
w := httptest.NewRecorder()
|
|
3623
|
+
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
|
3624
|
+
req.Header.Set("Origin", "https://example.com")
|
|
3625
|
+
newCORSRouter(t).ServeHTTP(w, req)
|
|
3626
|
+
|
|
3627
|
+
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "*" {
|
|
3628
|
+
t.Fatalf("expected ACAO=*, got %q", got)
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
func TestCORS_Preflight(t *testing.T) {
|
|
3633
|
+
t.Setenv("CORS_ALLOW_ORIGINS", "*")
|
|
3634
|
+
gin.SetMode(gin.TestMode)
|
|
3635
|
+
r := gin.New()
|
|
3636
|
+
r.Use(middleware.CORS())
|
|
3637
|
+
|
|
3638
|
+
w := httptest.NewRecorder()
|
|
3639
|
+
req, _ := http.NewRequest(http.MethodOptions, "/ping", nil)
|
|
3640
|
+
req.Header.Set("Origin", "https://example.com")
|
|
3641
|
+
req.Header.Set("Access-Control-Request-Method", "POST")
|
|
3642
|
+
r.ServeHTTP(w, req)
|
|
3643
|
+
|
|
3644
|
+
if w.Code != http.StatusNoContent {
|
|
3645
|
+
t.Fatalf("expected 204 preflight, got %d", w.Code)
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
func TestCORS_SpecificOrigin_Allowed(t *testing.T) {
|
|
3650
|
+
t.Setenv("CORS_ALLOW_ORIGINS", "https://app.example.com")
|
|
3651
|
+
w := httptest.NewRecorder()
|
|
3652
|
+
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
|
3653
|
+
req.Header.Set("Origin", "https://app.example.com")
|
|
3654
|
+
newCORSRouter(t).ServeHTTP(w, req)
|
|
3655
|
+
|
|
3656
|
+
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://app.example.com" {
|
|
3657
|
+
t.Fatalf("expected ACAO=https://app.example.com, got %q", got)
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
func TestCORS_SpecificOrigin_Denied(t *testing.T) {
|
|
3662
|
+
t.Setenv("CORS_ALLOW_ORIGINS", "https://app.example.com")
|
|
3663
|
+
w := httptest.NewRecorder()
|
|
3664
|
+
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
|
3665
|
+
req.Header.Set("Origin", "https://evil.com")
|
|
3666
|
+
newCORSRouter(t).ServeHTTP(w, req)
|
|
3667
|
+
|
|
3668
|
+
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
|
3669
|
+
t.Fatalf("expected no ACAO header for denied origin, got %q", got)
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
func TestCORS_Default_Origin(t *testing.T) {
|
|
3674
|
+
// When CORS_ALLOW_ORIGINS is unset the middleware must default to "*".
|
|
3675
|
+
t.Setenv("CORS_ALLOW_ORIGINS", "")
|
|
3676
|
+
w := httptest.NewRecorder()
|
|
3677
|
+
req, _ := http.NewRequest(http.MethodGet, "/ping", nil)
|
|
3678
|
+
req.Header.Set("Origin", "https://anywhere.com")
|
|
3679
|
+
newCORSRouter(t).ServeHTTP(w, req)
|
|
3680
|
+
|
|
3681
|
+
if got := w.Header().Get("Access-Control-Allow-Origin"); got == "" {
|
|
3682
|
+
t.Fatal("expected CORS header when CORS_ALLOW_ORIGINS defaults to *")
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
`}function hn(e){return `package server_test
|
|
3686
|
+
|
|
3687
|
+
import (
|
|
3688
|
+
"encoding/json"
|
|
3689
|
+
"net/http"
|
|
3690
|
+
"net/http/httptest"
|
|
3691
|
+
"testing"
|
|
3692
|
+
|
|
3693
|
+
"github.com/gin-gonic/gin"
|
|
3694
|
+
|
|
3695
|
+
"${e.module_path}/internal/config"
|
|
3696
|
+
"${e.module_path}/internal/server"
|
|
3697
|
+
)
|
|
3698
|
+
|
|
3699
|
+
func init() { gin.SetMode(gin.TestMode) }
|
|
3700
|
+
|
|
3701
|
+
type serverAPIError struct {
|
|
3702
|
+
Code string \`json:"code"\`
|
|
3703
|
+
Message string \`json:"message"\`
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
func TestServer_NotFound_JSON(t *testing.T) {
|
|
3707
|
+
w := httptest.NewRecorder()
|
|
3708
|
+
req, _ := http.NewRequest(http.MethodGet, "/no-such-route", nil)
|
|
3709
|
+
server.NewRouter(config.Load()).ServeHTTP(w, req)
|
|
3710
|
+
|
|
3711
|
+
if w.Code != http.StatusNotFound {
|
|
3712
|
+
t.Fatalf("expected 404, got %d", w.Code)
|
|
3713
|
+
}
|
|
3714
|
+
var body serverAPIError
|
|
3715
|
+
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
|
|
3716
|
+
t.Fatalf("expected JSON error body: %v", err)
|
|
3717
|
+
}
|
|
3718
|
+
if body.Code != "NOT_FOUND" {
|
|
3719
|
+
t.Fatalf("expected code=NOT_FOUND, got %q", body.Code)
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
func TestServer_MethodNotAllowed_JSON(t *testing.T) {
|
|
3724
|
+
w := httptest.NewRecorder()
|
|
3725
|
+
req, _ := http.NewRequest(http.MethodPost, "/api/v1/health/live", nil)
|
|
3726
|
+
server.NewRouter(config.Load()).ServeHTTP(w, req)
|
|
3727
|
+
|
|
3728
|
+
if w.Code != http.StatusMethodNotAllowed {
|
|
3729
|
+
t.Fatalf("expected 405, got %d", w.Code)
|
|
3730
|
+
}
|
|
3731
|
+
var body serverAPIError
|
|
3732
|
+
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
|
|
3733
|
+
t.Fatalf("expected JSON error body: %v", err)
|
|
3734
|
+
}
|
|
3735
|
+
if body.Code != "METHOD_NOT_ALLOWED" {
|
|
3736
|
+
t.Fatalf("expected code=METHOD_NOT_ALLOWED, got %q", body.Code)
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
func TestServer_CORS_Header(t *testing.T) {
|
|
3741
|
+
t.Setenv("CORS_ALLOW_ORIGINS", "*")
|
|
3742
|
+
w := httptest.NewRecorder()
|
|
3743
|
+
req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
|
|
3744
|
+
req.Header.Set("Origin", "https://example.com")
|
|
3745
|
+
server.NewRouter(config.Load()).ServeHTTP(w, req)
|
|
3746
|
+
|
|
3747
|
+
if got := w.Header().Get("Access-Control-Allow-Origin"); got == "" {
|
|
3748
|
+
t.Fatal("expected Access-Control-Allow-Origin header to be set")
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
func TestServer_HealthLive(t *testing.T) {
|
|
3753
|
+
w := httptest.NewRecorder()
|
|
3754
|
+
req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
|
|
3755
|
+
server.NewRouter(config.Load()).ServeHTTP(w, req)
|
|
3756
|
+
|
|
3757
|
+
if w.Code != http.StatusOK {
|
|
3758
|
+
t.Fatalf("expected 200, got %d", w.Code)
|
|
3759
|
+
}
|
|
3760
|
+
var body map[string]any
|
|
3761
|
+
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
|
|
3762
|
+
t.Fatalf("invalid JSON: %v", err)
|
|
3763
|
+
}
|
|
3764
|
+
if body["status"] != "ok" {
|
|
3765
|
+
t.Fatalf("expected status=ok, got %v", body["status"])
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
func TestServer_Docs_Redirect(t *testing.T) {
|
|
3770
|
+
w := httptest.NewRecorder()
|
|
3771
|
+
req, _ := http.NewRequest(http.MethodGet, "/docs", nil)
|
|
3772
|
+
server.NewRouter(config.Load()).ServeHTTP(w, req)
|
|
3773
|
+
|
|
3774
|
+
if w.Code != http.StatusFound {
|
|
3775
|
+
t.Fatalf("expected 302 redirect from /docs, got %d", w.Code)
|
|
3776
|
+
}
|
|
3777
|
+
if loc := w.Header().Get("Location"); loc != "/docs/index.html" {
|
|
3778
|
+
t.Fatalf("expected Location=/docs/index.html, got %q", loc)
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
func TestServer_ReleaseMode(t *testing.T) {
|
|
3783
|
+
// Covers the gin.SetMode(gin.ReleaseMode) branch in NewRouter.
|
|
3784
|
+
// Restore test mode after so other tests are not affected.
|
|
3785
|
+
t.Cleanup(func() { gin.SetMode(gin.TestMode) })
|
|
3786
|
+
t.Setenv("GIN_MODE", "release")
|
|
3787
|
+
w := httptest.NewRecorder()
|
|
3788
|
+
req, _ := http.NewRequest(http.MethodGet, "/api/v1/health/live", nil)
|
|
3789
|
+
server.NewRouter(config.Load()).ServeHTTP(w, req)
|
|
3790
|
+
|
|
3791
|
+
if w.Code != http.StatusOK {
|
|
3792
|
+
t.Fatalf("expected 200 in release mode, got %d", w.Code)
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
`}function wn(e){return `package middleware
|
|
3796
|
+
|
|
3797
|
+
import (
|
|
3798
|
+
"os"
|
|
3799
|
+
"strconv"
|
|
3800
|
+
"sync"
|
|
3801
|
+
"time"
|
|
3802
|
+
|
|
3803
|
+
"github.com/gin-gonic/gin"
|
|
3804
|
+
|
|
3805
|
+
"${e.module_path}/internal/apierr"
|
|
3806
|
+
)
|
|
3807
|
+
|
|
3808
|
+
// ipBucket tracks the per-IP fixed-window request counter.
|
|
3809
|
+
type ipBucket struct {
|
|
3810
|
+
mu sync.Mutex
|
|
3811
|
+
count int
|
|
3812
|
+
windowStart time.Time
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
// RateLimit returns a per-IP fixed-window rate limiter.
|
|
3816
|
+
// Configure the limit via RATE_LIMIT_RPS env var (requests per second, default 100).
|
|
3817
|
+
func RateLimit() gin.HandlerFunc {
|
|
3818
|
+
rps := 100
|
|
3819
|
+
if raw := os.Getenv("RATE_LIMIT_RPS"); raw != "" {
|
|
3820
|
+
if n, err := strconv.Atoi(raw); err == nil && n > 0 {
|
|
3821
|
+
rps = n
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
var buckets sync.Map
|
|
3825
|
+
return func(c *gin.Context) {
|
|
3826
|
+
ip := c.ClientIP()
|
|
3827
|
+
now := time.Now()
|
|
3828
|
+
v, _ := buckets.LoadOrStore(ip, &ipBucket{windowStart: now})
|
|
3829
|
+
b := v.(*ipBucket)
|
|
3830
|
+
b.mu.Lock()
|
|
3831
|
+
if now.Sub(b.windowStart) >= time.Second {
|
|
3832
|
+
b.count = 0
|
|
3833
|
+
b.windowStart = now
|
|
3834
|
+
}
|
|
3835
|
+
b.count++
|
|
3836
|
+
count := b.count
|
|
3837
|
+
b.mu.Unlock()
|
|
3838
|
+
if count > rps {
|
|
3839
|
+
apierr.TooManyRequests(c)
|
|
3840
|
+
c.Header("Retry-After", "1")
|
|
3841
|
+
return
|
|
3842
|
+
}
|
|
3843
|
+
c.Next()
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
`}function yn(e){return `package middleware_test
|
|
3847
|
+
|
|
3848
|
+
import (
|
|
3849
|
+
"net/http"
|
|
3850
|
+
"net/http/httptest"
|
|
3851
|
+
"testing"
|
|
3852
|
+
|
|
3853
|
+
"github.com/gin-gonic/gin"
|
|
3854
|
+
|
|
3855
|
+
"${e.module_path}/internal/middleware"
|
|
3856
|
+
)
|
|
3857
|
+
|
|
3858
|
+
func newRateLimitRouter(t *testing.T) *gin.Engine {
|
|
3859
|
+
t.Helper()
|
|
3860
|
+
gin.SetMode(gin.TestMode)
|
|
3861
|
+
r := gin.New()
|
|
3862
|
+
r.Use(middleware.RateLimit())
|
|
3863
|
+
r.GET("/", func(c *gin.Context) { c.Status(http.StatusOK) })
|
|
3864
|
+
return r
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
func TestRateLimit_AllowsUnderLimit(t *testing.T) {
|
|
3868
|
+
t.Setenv("RATE_LIMIT_RPS", "3")
|
|
3869
|
+
r := newRateLimitRouter(t)
|
|
3870
|
+
|
|
3871
|
+
for i := 0; i < 3; i++ {
|
|
3872
|
+
w := httptest.NewRecorder()
|
|
3873
|
+
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
|
3874
|
+
r.ServeHTTP(w, req)
|
|
3875
|
+
if w.Code != http.StatusOK {
|
|
3876
|
+
t.Fatalf("request %d: expected 200, got %d", i+1, w.Code)
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
func TestRateLimit_Blocks_After_Limit(t *testing.T) {
|
|
3882
|
+
t.Setenv("RATE_LIMIT_RPS", "2")
|
|
3883
|
+
r := newRateLimitRouter(t)
|
|
3884
|
+
|
|
3885
|
+
// Exhaust the limit.
|
|
3886
|
+
for i := 0; i < 2; i++ {
|
|
3887
|
+
w := httptest.NewRecorder()
|
|
3888
|
+
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
|
3889
|
+
r.ServeHTTP(w, req)
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
// Next request must be rejected.
|
|
3893
|
+
w := httptest.NewRecorder()
|
|
3894
|
+
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
|
3895
|
+
r.ServeHTTP(w, req)
|
|
3896
|
+
|
|
3897
|
+
if w.Code != http.StatusTooManyRequests {
|
|
3898
|
+
t.Fatalf("expected 429 after limit, got %d", w.Code)
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
func TestRateLimit_InvalidRPS(t *testing.T) {
|
|
3903
|
+
// When RATE_LIMIT_RPS is not a valid positive integer, the middleware
|
|
3904
|
+
// must fall back to the default limit (100 rps) and allow normal requests.
|
|
3905
|
+
t.Setenv("RATE_LIMIT_RPS", "not-a-number")
|
|
3906
|
+
r := newRateLimitRouter(t)
|
|
3907
|
+
w := httptest.NewRecorder()
|
|
3908
|
+
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
|
3909
|
+
r.ServeHTTP(w, req)
|
|
3910
|
+
|
|
3911
|
+
if w.Code != http.StatusOK {
|
|
3912
|
+
t.Fatalf("expected 200 with invalid RPS env, got %d", w.Code)
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
`}function vn(e){return `# Air \u2014 live reload for Go projects
|
|
3916
|
+
# https://github.com/air-verse/air
|
|
3917
|
+
root = "."
|
|
3918
|
+
tmp_dir = "tmp"
|
|
3919
|
+
|
|
3920
|
+
[build]
|
|
3921
|
+
pre_cmd = ["$(go env GOPATH)/bin/swag init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null || true"]
|
|
3922
|
+
cmd = "go build -o ./tmp/server ./cmd/server"
|
|
3923
|
+
bin = "./tmp/server"
|
|
3924
|
+
include_ext = ["go", "yaml", "yml", "env"]
|
|
3925
|
+
exclude_dir = ["tmp", "vendor", ".git", "testdata", "docs"]
|
|
3926
|
+
delay = 500
|
|
3927
|
+
rerun_delay = 500
|
|
3928
|
+
send_interrupt = true
|
|
3929
|
+
kill_delay = "200ms"
|
|
3930
|
+
|
|
3931
|
+
[env]
|
|
3932
|
+
GIN_MODE = "debug"
|
|
3933
|
+
PORT = "${e.port}"
|
|
3934
|
+
|
|
3935
|
+
[misc]
|
|
3936
|
+
clean_on_exit = true
|
|
3937
|
+
|
|
3938
|
+
[log]
|
|
3939
|
+
time = false
|
|
3940
|
+
`}function bn(e){return `run:
|
|
3941
|
+
timeout: 5m
|
|
3942
|
+
|
|
3943
|
+
linters:
|
|
3944
|
+
enable:
|
|
3945
|
+
- bodyclose
|
|
3946
|
+
- durationcheck
|
|
3947
|
+
- errcheck
|
|
3948
|
+
- errname
|
|
3949
|
+
- errorlint
|
|
3950
|
+
- gci
|
|
3951
|
+
- goimports
|
|
3952
|
+
- gosimple
|
|
3953
|
+
- govet
|
|
3954
|
+
- ineffassign
|
|
3955
|
+
- misspell
|
|
3956
|
+
- noctx
|
|
3957
|
+
- nolintlint
|
|
3958
|
+
- prealloc
|
|
3959
|
+
- staticcheck
|
|
3960
|
+
- unconvert
|
|
3961
|
+
- unused
|
|
3962
|
+
- wrapcheck
|
|
3963
|
+
|
|
3964
|
+
linters-settings:
|
|
3965
|
+
gci:
|
|
3966
|
+
sections:
|
|
3967
|
+
- standard
|
|
3968
|
+
- default
|
|
3969
|
+
- prefix(${e})
|
|
3970
|
+
goimports:
|
|
3971
|
+
local-prefixes: "${e}"
|
|
3972
|
+
govet:
|
|
3973
|
+
enable:
|
|
3974
|
+
- shadow
|
|
3975
|
+
wrapcheck:
|
|
3976
|
+
ignorePackageGlobs:
|
|
3977
|
+
- "${e}/*"
|
|
3978
|
+
|
|
3979
|
+
issues:
|
|
3980
|
+
max-same-issues: 5
|
|
3981
|
+
exclude-rules:
|
|
3982
|
+
- path: _test.go
|
|
3983
|
+
linters:
|
|
3984
|
+
- errcheck
|
|
3985
|
+
- wrapcheck
|
|
3986
|
+
`}function kn(){return JSON.stringify({engine:"npm",runtime:"go"},null,2)}function Rn(e,o){return JSON.stringify({kit_name:"gogin.standard",runtime:"go",module_support:false,project_name:e.project_name,module_path:e.module_path,app_version:e.app_version,created_by:"rapidkit-npm",rapidkit_version:o,created_at:new Date().toISOString()},null,2)}function xn(e){return `#!/usr/bin/env sh
|
|
3987
|
+
# RapidKit Go/Gin project launcher \u2014 generated by RapidKit CLI
|
|
3988
|
+
# https://getrapidkit.com
|
|
3989
|
+
|
|
3990
|
+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
3991
|
+
CMD="\${1:-}"
|
|
3992
|
+
shift 2>/dev/null || true
|
|
3993
|
+
|
|
3994
|
+
case "$CMD" in
|
|
3995
|
+
init)
|
|
3996
|
+
cd "$SCRIPT_DIR"
|
|
3997
|
+
echo "\u{1F439} Initializing Go/Gin project\u2026"
|
|
3998
|
+
GOBIN="$(go env GOPATH)/bin"
|
|
3999
|
+
echo " \u2192 installing air (hot reload)\u2026"
|
|
4000
|
+
go install github.com/air-verse/air@latest 2>/dev/null && echo " \u2713 air" || echo " \u26A0 air install failed (run: go install github.com/air-verse/air@latest)"
|
|
4001
|
+
echo " \u2192 installing swag (swagger)\u2026"
|
|
4002
|
+
go install github.com/swaggo/swag/cmd/swag@latest 2>/dev/null && echo " \u2713 swag" || echo " \u26A0 swag install failed (run: go install github.com/swaggo/swag/cmd/swag@latest)"
|
|
4003
|
+
if [ ! -f ".env" ] && [ -f ".env.example" ]; then
|
|
4004
|
+
cp .env.example .env && echo " \u2713 .env created from .env.example"
|
|
4005
|
+
fi
|
|
4006
|
+
go mod tidy && echo " \u2713 go mod tidy"
|
|
4007
|
+
echo " \u2192 generating swagger docs (first build)\u2026"
|
|
4008
|
+
"$(go env GOPATH)/bin/swag" init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null && echo " \u2713 swagger docs generated" || echo " \u26A0 swagger docs skipped (run: rapidkit docs)"
|
|
4009
|
+
echo "\u2705 Ready \u2014 run: rapidkit dev"
|
|
4010
|
+
;;
|
|
4011
|
+
dev)
|
|
4012
|
+
cd "$SCRIPT_DIR"
|
|
4013
|
+
echo "\u{1F4D6} Syncing swagger docs\u2026"
|
|
4014
|
+
"$(go env GOPATH)/bin/swag" init -g main.go -d cmd/server,internal/handlers,internal/apierr -o docs --parseDependency 2>/dev/null || true
|
|
4015
|
+
if [ -f "$SCRIPT_DIR/Makefile" ]; then
|
|
4016
|
+
exec make -C "$SCRIPT_DIR" dev "$@"
|
|
4017
|
+
else
|
|
4018
|
+
cd "$SCRIPT_DIR" && GIN_MODE=debug exec go run ./cmd/server "$@"
|
|
4019
|
+
fi
|
|
4020
|
+
;;
|
|
4021
|
+
start)
|
|
4022
|
+
BIN="$SCRIPT_DIR/bin/${e.project_name}"
|
|
4023
|
+
if [ ! -f "$BIN" ]; then
|
|
4024
|
+
make -C "$SCRIPT_DIR" build
|
|
4025
|
+
fi
|
|
4026
|
+
exec "$BIN" "$@"
|
|
4027
|
+
;;
|
|
4028
|
+
build)
|
|
4029
|
+
exec make -C "$SCRIPT_DIR" build "$@"
|
|
4030
|
+
;;
|
|
4031
|
+
test)
|
|
4032
|
+
exec make -C "$SCRIPT_DIR" test "$@"
|
|
4033
|
+
;;
|
|
4034
|
+
lint)
|
|
4035
|
+
exec make -C "$SCRIPT_DIR" lint "$@"
|
|
4036
|
+
;;
|
|
4037
|
+
format|fmt)
|
|
4038
|
+
exec make -C "$SCRIPT_DIR" fmt "$@"
|
|
4039
|
+
;;
|
|
4040
|
+
docs)
|
|
4041
|
+
exec make -C "$SCRIPT_DIR" docs "$@"
|
|
4042
|
+
;;
|
|
4043
|
+
help|--help|-h)
|
|
4044
|
+
echo "RapidKit \u2014 Go/Gin project: ${e.project_name}"
|
|
4045
|
+
echo ""
|
|
4046
|
+
echo "Usage: rapidkit <command>"
|
|
4047
|
+
echo ""
|
|
4048
|
+
echo " init Install tools + create .env (air, swag, go mod tidy)"
|
|
4049
|
+
echo " dev Hot reload dev server (make dev \u2014 requires air)"
|
|
4050
|
+
echo " start Run compiled binary (make build + bin)"
|
|
4051
|
+
echo " build Build binary (make build)"
|
|
4052
|
+
echo " docs Generate Swagger docs (make docs \u2014 requires swag)"
|
|
4053
|
+
echo " test Run tests (make test)"
|
|
4054
|
+
echo " lint Run linter (make lint)"
|
|
4055
|
+
echo " format Format code (make fmt)"
|
|
4056
|
+
;;
|
|
4057
|
+
*)
|
|
4058
|
+
if [ -n "$CMD" ]; then
|
|
4059
|
+
echo "rapidkit: unknown command: $CMD" >&2
|
|
4060
|
+
fi
|
|
4061
|
+
echo "Available: init, dev, start, build, docs, test, lint, format" >&2
|
|
4062
|
+
exit 1
|
|
4063
|
+
;;
|
|
4064
|
+
esac
|
|
4065
|
+
`}function Cn(e){return `@echo off
|
|
4066
|
+
rem RapidKit Go/Gin project launcher \u2014 Windows
|
|
4067
|
+
set CMD=%1
|
|
4068
|
+
if "%CMD%"=="" goto usage
|
|
4069
|
+
shift
|
|
4070
|
+
|
|
4071
|
+
if "%CMD%"=="init" (
|
|
4072
|
+
echo Initializing Go/Gin project...
|
|
4073
|
+
go install github.com/air-verse/air@latest
|
|
4074
|
+
go install github.com/swaggo/swag/cmd/swag@latest
|
|
4075
|
+
if not exist .env if exist .env.example copy .env.example .env
|
|
4076
|
+
go mod tidy
|
|
4077
|
+
exit /b %ERRORLEVEL%
|
|
4078
|
+
)
|
|
4079
|
+
if "%CMD%"=="dev" ( make dev %* & exit /b %ERRORLEVEL% )
|
|
4080
|
+
if "%CMD%"=="build" ( make build %* & exit /b %ERRORLEVEL% )
|
|
4081
|
+
if "%CMD%"=="test" ( make test %* & exit /b %ERRORLEVEL% )
|
|
4082
|
+
if "%CMD%"=="lint" ( make lint %* & exit /b %ERRORLEVEL% )
|
|
4083
|
+
if "%CMD%"=="format" ( make fmt %* & exit /b %ERRORLEVEL% )
|
|
4084
|
+
if "%CMD%"=="docs" ( make docs %* & exit /b %ERRORLEVEL% )
|
|
4085
|
+
if "%CMD%"=="start" ( bin\\${e.project_name}.exe %* & exit /b %ERRORLEVEL% )
|
|
4086
|
+
|
|
4087
|
+
:usage
|
|
4088
|
+
echo Available: init, dev, start, build, docs, test, lint, format
|
|
4089
|
+
exit /b 1
|
|
4090
|
+
`}async function mt(e,o){let t={project_name:o.project_name,module_path:o.module_path||o.project_name,author:o.author||"RapidKit User",description:o.description||`Go/Gin REST API \u2014 ${o.project_name}`,go_version:o.go_version||"1.24",app_version:o.app_version||"0.1.0",port:o.port||"8080",skipGit:o.skipGit??false},r=c();try{await execa("go",["version"],{timeout:3e3});}catch{console.log(g.yellow("\n\u26A0 Go not found in PATH \u2014 project will be scaffolded, but `go mod tidy` requires Go 1.21+")),console.log(g.gray(` Install: https://go.dev/dl/
|
|
4091
|
+
`));}let i=pt(`Generating Go/Gin project: ${t.project_name}\u2026`).start();try{let n=(a,l)=>Vr(v.join(e,a),l),s=v.join(e,"rapidkit"),c=v.join(e,"rapidkit.cmd");await Promise.all([n("cmd/server/main.go",Wr(t)),n("go.mod",Br(t)),n("internal/config/config.go",Jr(t)),n("internal/server/server.go",zr(t)),n("internal/middleware/requestid.go",sn()),n("internal/middleware/requestid_test.go",an(t)),n("internal/apierr/apierr.go",cn()),n("internal/apierr/apierr_test.go",dn(t)),n("internal/handlers/health.go",Yr()),n("internal/handlers/health_test.go",Qr(t)),n("internal/handlers/example.go",pn(t)),n("internal/handlers/example_test.go",un(t)),n("internal/config/config_test.go",gn(t)),n("internal/middleware/cors.go",mn()),n("internal/middleware/cors_test.go",fn(t)),n("internal/middleware/ratelimit.go",wn(t)),n("internal/middleware/ratelimit_test.go",yn(t)),n("internal/server/server_test.go",hn(t)),n("docs/doc.go",ln(t)),n(".air.toml",vn(t)),n("Dockerfile",Xr()),n("docker-compose.yml",Zr(t)),n("Makefile",en(t)),n(".golangci.yml",bn(t.module_path)),n(".env.example",tn(t)),n(".gitignore",on()),n(".github/workflows/ci.yml",rn(t)),n("README.md",nn(t)),n(".rapidkit/project.json",Rn(t,r)),n(".rapidkit/context.json",kn()),n("rapidkit",xn(t)),n("rapidkit.cmd",Cn(t))]),await promises.chmod(s,493),await promises.chmod(c,493),i.succeed(g.green(`Project created at ${e}`));try{i.start("Fetching Go dependencies\u2026"),await execa("go",["mod","tidy"],{cwd:e,timeout:12e4}),i.succeed(g.gray("\u2713 go mod tidy completed"));}catch{i.warn(g.yellow("\u26A0 go mod tidy failed \u2014 run manually: go mod tidy"));}if(!t.skipGit)try{await execa("git",["init"],{cwd:e}),await execa("git",["add","-A"],{cwd:e}),await execa("git",["commit","-m","chore: initial scaffold (rapidkit gogin.standard)"],{cwd:e}),console.log(g.gray("\u2713 git repository initialized"));}catch{console.log(g.gray("\u26A0 git init skipped (git not found or error)"));}console.log(""),console.log(g.bold("\u2705 Go/Gin project ready!")),console.log(""),console.log(g.cyan("Next steps:")),console.log(g.white(` cd ${t.project_name}`)),console.log(g.white(" make run # start dev server")),console.log(g.white(" make test # run tests")),console.log(""),console.log(g.gray("Server will listen on port "+t.port)),console.log(g.gray(" http://localhost:"+t.port+"/api/v1/health/live")),console.log(g.gray(" http://localhost:"+t.port+"/api/v1/health/ready")),console.log(""),console.log(g.yellow("\u2139 RapidKit modules are not available for Go projects (module system uses Python/pip).")),console.log("");}catch(n){throw i.fail(g.red("Failed to generate Go/Gin project")),n}}async function Yt(){let e=process.platform==="win32"?["python","python3"]:["python3","python"];for(let o of e)try{let{stdout:t}=await execa(o,["--version"],{timeout:3e3}),r=t.match(/Python (\d+\.\d+\.\d+)/);if(r){let i=r[1],[n,s]=i.split(".").map(Number);return n<3||n===3&&s<10?{status:"warn",message:`Python ${i} (requires 3.10+)`,details:`${o} found but version is below minimum requirement`}:{status:"ok",message:`Python ${i}`,details:`Using ${o}`}}}catch{continue}return {status:"error",message:"Python not found",details:"Install Python 3.10+ and ensure it's in PATH"}}async function Qt(){try{let{stdout:e}=await execa("poetry",["--version"],{timeout:3e3}),o=e.match(/Poetry .*version ([\d.]+)/);return o?{status:"ok",message:`Poetry ${o[1]}`,details:"Available for dependency management"}:{status:"warn",message:"Poetry version unknown"}}catch{return {status:"warn",message:"Poetry not installed",details:"Optional: Install for better dependency management"}}}async function Xt(){try{let{stdout:e}=await execa("pipx",["--version"],{timeout:3e3});return {status:"ok",message:`pipx ${e.trim()}`,details:"Available for global tool installation"}}catch{return {status:"warn",message:"pipx not installed",details:"Optional: Install for isolated Python tools"}}}async function Zt(){try{let{stdout:e}=await execa("go",["version"],{timeout:3e3}),o=e.match(/go version go(\d+\.\d+(?:\.\d+)?)/);return o?{status:"ok",message:`Go ${o[1]}`,details:"Available for Go/Fiber and Go/Gin projects"}:{status:"ok",message:"Go (version unknown)",details:"go found in PATH"}}catch{return {status:"warn",message:"Go not installed",details:"Optional: Required only for gofiber.standard / gogin.standard projects \u2014 https://go.dev/dl/"}}}async function eo(){let e=process.env.HOME||process.env.USERPROFILE||"",o=[],t=[{location:"Global (pipx)",path:v.join(e,".local","bin","rapidkit")},{location:"Global (pipx)",path:v.join(e,"AppData","Roaming","Python","Scripts","rapidkit.exe")},{location:"Global (pyenv)",path:v.join(e,".pyenv","shims","rapidkit")},{location:"Global (system)",path:"/usr/local/bin/rapidkit"},{location:"Global (system)",path:"/usr/bin/rapidkit"}],r=[{location:"Workspace (.venv)",path:v.join(process.cwd(),".venv","bin","rapidkit")},{location:"Workspace (.venv)",path:v.join(process.cwd(),".venv","Scripts","rapidkit.exe")}];for(let{location:n,path:s}of [...t,...r])try{if(await R__default.pathExists(s)){let{stdout:c,exitCode:a}=await execa(s,["--version"],{timeout:3e3,reject:false});if(a===0&&(c.includes("RapidKit Version")||c.includes("RapidKit"))){let l=c.match(/v?([\d.]+(?:rc\d+)?(?:a\d+)?(?:b\d+)?)/);l&&o.push({location:n,path:s,version:l[1]});}}}catch{continue}if(o.length>0)return {status:"ok",message:`RapidKit Core ${o[0].version}`,paths:o.map(s=>({location:s.location,path:s.path,version:s.version}))};try{let{stdout:n,exitCode:s}=await execa("rapidkit",["--version"],{timeout:3e3,reject:false});if(s===0&&(n.includes("RapidKit Version")||n.includes("RapidKit"))){let c=n.match(/v?([\d.]+(?:rc\d+)?(?:a\d+)?(?:b\d+)?)/);if(c)return {status:"ok",message:`RapidKit Core ${c[1]}`,details:"Available via PATH"}}}catch{}try{let{stdout:n,exitCode:s}=await execa("poetry",["run","rapidkit","--version"],{timeout:3e3,reject:false});if(s===0&&(n.includes("RapidKit Version")||n.includes("RapidKit"))){let c=n.match(/v?([\d.]+(?:rc\d+)?(?:a\d+)?(?:b\d+)?)/);if(c)return {status:"ok",message:`RapidKit Core ${c[1]}`,details:"Available via Poetry"}}}catch{}let i=process.platform==="win32"?["python","python3"]:["python3","python"];for(let n of i)try{let{stdout:s,exitCode:c}=await execa(n,["-c","import rapidkit_core; print(rapidkit_core.__version__)"],{timeout:3e3,reject:false});if(c===0&&s&&!s.includes("Traceback")&&!s.includes("ModuleNotFoundError")){let a=s.trim();if(a)return {status:"ok",message:`RapidKit Core ${a}`,details:`Available in ${n} environment`}}}catch{continue}return {status:"error",message:"RapidKit Core not installed",details:"Install with: pipx install rapidkit-core"}}async function Fe(e,o){let t=v.join(e,"Dockerfile");o.hasDocker=await R__default.pathExists(t);let r=v.join(e,"tests"),i=v.join(e,"test"),n=await R__default.pathExists(r)||await R__default.pathExists(i),s=false;if(o.framework==="Go/Fiber"||o.framework==="Go/Gin")try{let c=[{dir:e,depth:0}],a=4,l=new Set([".git",".venv","node_modules","dist","build","vendor"]);for(;c.length>0&&!s;){let d=c.shift();if(!d)break;let p=[];try{p=await R__default.readdir(d.dir);}catch{continue}for(let u of p){let h=v.join(d.dir,u),x;try{x=await R__default.stat(h);}catch{continue}if(x.isFile()&&u.endsWith("_test.go")){s=true;break}x.isDirectory()&&d.depth<a&&!l.has(u)&&!u.startsWith(".")&&c.push({dir:h,depth:d.depth+1});}}}catch{}if(o.hasTests=n||s,o.framework==="NestJS"){let c=v.join(e,".eslintrc.js"),a=v.join(e,".eslintrc.json");o.hasCodeQuality=await R__default.pathExists(c)||await R__default.pathExists(a);}else if(o.framework==="Go/Fiber"||o.framework==="Go/Gin"){let c=v.join(e,".golangci.yml"),a=v.join(e,".golangci.yaml"),l=v.join(e,"Makefile"),d=await R__default.pathExists(l)&&(await R__default.readFile(l,"utf8")).includes("golangci-lint");o.hasCodeQuality=await R__default.pathExists(c)||await R__default.pathExists(a)||d;}else if(o.framework==="FastAPI"){let c=v.join(e,"ruff.toml"),a=v.join(e,"pyproject.toml");if(await R__default.pathExists(a))try{let l=await R__default.readFile(a,"utf8");o.hasCodeQuality=l.includes("[tool.ruff]")||await R__default.pathExists(c);}catch{o.hasCodeQuality=await R__default.pathExists(c);}}try{if(o.framework==="NestJS"){let{stdout:c}=await execa("npm",["audit","--json"],{cwd:e,reject:false});if(c)try{let l=JSON.parse(c).metadata?.vulnerabilities;l&&(o.vulnerabilities=(l.high||0)+(l.critical||0)+(l.moderate||0));}catch{}}else if(o.framework==="FastAPI"){let c=v.join(e,".venv"),a=process.platform==="win32"?v.join(c,"Scripts","python.exe"):v.join(c,"bin","python");if(await R__default.pathExists(a))try{let{stdout:l}=await execa(a,["-m","pip","list","--format=json"],{timeout:5e3,reject:false});if(l){JSON.parse(l);o.vulnerabilities=0;}}catch{}}}catch{}}async function _n(e){let t={name:v.basename(e),path:e,venvActive:false,depsInstalled:false,coreInstalled:false,issues:[],fixCommands:[]},r=v.join(e,".rapidkit");if(!await R__default.pathExists(r))return t.issues.push("Not a valid RapidKit project (missing .rapidkit directory)"),t;try{let p=v.join(e,"registry.json");if(await R__default.pathExists(p)){let u=await R__default.readJson(p);u.installed_modules&&(t.stats={modules:u.installed_modules.length});}}catch{}let i=null;try{let p=v.join(r,"project.json");if(await R__default.pathExists(p)){i=await R__default.readJson(p);let u=i?.kit_name||i?.kit;u&&(t.kit=u);}}catch{}try{let p=v.join(e,".git");if(await R__default.pathExists(p)){let{stdout:u}=await execa("git",["log","-1","--format=%cr"],{cwd:e,reject:false});u&&(t.lastModified=u.trim());}else {let u=await R__default.stat(e),x=Date.now()-u.mtime.getTime(),y=Math.floor(x/(1e3*60*60*24));t.lastModified=y===0?"today":`${y} day${y>1?"s":""} ago`;}}catch{}let n=v.join(e,"package.json"),s=v.join(e,"pyproject.toml"),c=v.join(e,"go.mod");if(await R__default.pathExists(c)||i?.runtime==="go"||typeof i?.kit_name=="string"&&(i.kit_name.startsWith("gofiber")||i.kit_name.startsWith("gogin"))){let p=i?.kit_name??"";t.framework=p.startsWith("gogin")?"Go/Gin":"Go/Fiber",t.isGoProject=true,t.venvActive=true,t.coreInstalled=false;try{await execa("go",["version"],{timeout:3e3});}catch{t.issues.push("Go toolchain not found \u2014 install from https://go.dev/dl/"),t.fixCommands?.push("https://go.dev/dl/");}let u=v.join(e,"go.sum");return await R__default.pathExists(u)?t.depsInstalled=true:(t.depsInstalled=false,t.issues.push("Go dependencies not downloaded (go.sum missing)"),t.fixCommands?.push(`cd ${e} && go mod tidy`)),await Fe(e,t),t}let l=await R__default.pathExists(n),d=await R__default.pathExists(s);if(l){t.framework="NestJS",t.venvActive=true;let p=v.join(e,"node_modules");if(await R__default.pathExists(p))try{let y=(await R__default.readdir(p)).filter(C=>!C.startsWith(".")&&!C.startsWith("_"));t.depsInstalled=y.length>0;}catch{t.depsInstalled=false;}t.depsInstalled||(t.issues.push("Dependencies not installed (node_modules empty or missing)"),t.fixCommands?.push(`cd ${e} && rapidkit init`)),t.coreInstalled=false;let u=v.join(e,".env");if(t.hasEnvFile=await R__default.pathExists(u),!t.hasEnvFile){let x=v.join(e,".env.example");await R__default.pathExists(x)&&(t.issues.push("Environment file missing (found .env.example)"),t.fixCommands?.push(`cd ${e} && cp .env.example .env`));}let h=v.join(e,"src");if(t.modulesHealthy=true,t.missingModules=[],await R__default.pathExists(h))try{let x=await R__default.readdir(h);t.modulesHealthy=x.length>0;}catch{t.modulesHealthy=false;}return await Fe(e,t),t}if(d){t.framework="FastAPI";let p=v.join(e,".venv");if(await R__default.pathExists(p)){t.venvActive=true;let y=process.platform==="win32"?v.join(p,"Scripts","python.exe"):v.join(p,"bin","python");if(await R__default.pathExists(y)){try{let{stdout:C}=await execa(y,["-c","import rapidkit_core; print(rapidkit_core.__version__)"],{timeout:2e3});t.coreInstalled=true,t.coreVersion=C.trim();}catch{t.coreInstalled=false;}try{await execa(y,["-c","import fastapi"],{timeout:2e3}),t.depsInstalled=true;}catch{try{let C=v.join(p,"lib");if(await R__default.pathExists(C)){let q=(await R__default.readdir(C)).find(V=>V.startsWith("python"));if(q){let V=v.join(C,q,"site-packages");if(await R__default.pathExists(V)){let st=(await R__default.readdir(V)).filter(at=>!at.startsWith("_")&&!at.includes("dist-info")&&!["pip","setuptools","wheel","pkg_resources"].includes(at));t.depsInstalled=st.length>0;}}}t.depsInstalled||(t.issues.push("Dependencies not installed"),t.fixCommands?.push(`cd ${e} && rapidkit init`));}catch{t.issues.push("Could not verify dependency installation");}}}else t.issues.push("Virtual environment exists but Python executable not found");}else t.issues.push("Virtual environment not created"),t.fixCommands?.push(`cd ${e} && rapidkit init`);let u=v.join(e,".env");if(t.hasEnvFile=await R__default.pathExists(u),!t.hasEnvFile){let y=v.join(e,".env.example");await R__default.pathExists(y)&&(t.issues.push("Environment file missing (found .env.example)"),t.fixCommands?.push(`cd ${e} && cp .env.example .env`));}let h=v.join(e,"src"),x=v.join(e,"modules");if(t.modulesHealthy=true,t.missingModules=[],await R__default.pathExists(h)){let y=v.join(h,"__init__.py");await R__default.pathExists(y)||(t.modulesHealthy=false,t.missingModules.push("src/__init__.py"));}if(await R__default.pathExists(x))try{let y=await to(x);for(let C of y){let E=v.join(x,C,"__init__.py");await R__default.pathExists(E)||(t.modulesHealthy=false,t.missingModules.push(`modules/${C}/__init__.py`));}}catch{}return !t.modulesHealthy&&t.missingModules.length>0&&t.issues.push(`Missing module init files: ${t.missingModules.join(", ")}`),await Fe(e,t),t}return t.issues.push("Unknown project type (no package.json or pyproject.toml)"),await Fe(e,t),t}async function to(e){try{return (await R__default.readdir(e,{withFileTypes:true})).filter(t=>t.isDirectory()).map(t=>t.name)}catch{try{let o=await R__default.readdir(e),t=[];for(let r of o)try{(await R__default.stat(v.join(e,r))).isDirectory()&&t.push(r);}catch{continue}return t}catch{return []}}}async function ft(e){let o=v.join(e,".rapidkit");if(!await R__default.pathExists(o))return false;let t=["project.json","context.json","file-hashes.json"];for(let r of t)if(await R__default.pathExists(v.join(o,r)))return true;return false}function oo(e,o){if(o.has(e))return true;let t=e.toLowerCase();return !!(t==="dist"||t.startsWith("dist-")||t.startsWith("dist_")||t==="build"||t.startsWith("build-")||t.startsWith("build_"))}async function Pn(e,o,t){let r=new Set,i=[{dir:e,depth:0}];for(;i.length>0;){let n=i.shift();if(!n)break;try{let s=await R__default.readdir(n.dir);for(let c of s){if(oo(c,t))continue;let a=v.join(n.dir,c),l;try{l=await R__default.stat(a);}catch{continue}if(l.isDirectory()){if(await ft(a)){r.add(a);continue}n.depth<o&&i.push({dir:a,depth:n.depth+1});}}}catch{continue}}return Array.from(r)}async function In(e){let o=e,t=v.parse(o).root;for(;o!==t;){let r=[v.join(o,".rapidkit-workspace"),v.join(o,".rapidkit","workspace-marker.json"),v.join(o,".rapidkit","config.json")];for(let i of r)if(await R__default.pathExists(i))return o;o=v.dirname(o);}return null}function En(e,o){let t=0,r=0,i=0;return e.forEach(s=>{s.status==="ok"?t++:s.status==="warn"?r++:s.status==="error"&&i++;}),o.forEach(s=>{(s.isGoProject?s.issues.length===0&&s.depsInstalled:s.issues.length===0&&s.venvActive&&s.depsInstalled)?t++:s.issues.length>0&&r++;}),{total:t+r+i,passed:t,warnings:r,errors:i}}async function Tn(e){let o=v.basename(e);try{let i=v.join(e,".rapidkit-workspace");await R__default.pathExists(i)&&(o=(await R__default.readJSON(i)).name||o);}catch{try{let i=v.join(e,".rapidkit","config.json");o=(await R__default.readJSON(i)).workspace_name||o;}catch{}}let t={workspacePath:e,workspaceName:o,python:await Yt(),poetry:await Qt(),pipx:await Xt(),go:await Zt(),rapidkitCore:await eo(),projects:[]};try{let i=new Set([".git",".venv","node_modules",".rapidkit","dist","build","coverage","__pycache__"]),n=new Set;await ft(e)&&n.add(e);let s=async(c,a)=>{if(a<0)return;let l=await to(c);for(let d of l){if(oo(d,i))continue;let p=v.join(c,d);if(await ft(p)){n.add(p);continue}a>0&&await s(p,a-1);}};if(await s(e,1),a.debug(`Workspace scan (shallow) found ${n.size} project(s)`),n.size===0){let c=await Pn(e,3,i);c.forEach(a=>n.add(a)),a.debug(`Workspace scan (deep fallback) found ${c.length} project(s)`);}n.size>0&&a.debug(`Workspace projects detected: ${Array.from(n).join(", ")}`);for(let c of n){let a=await _n(c);t.projects.push(a);}}catch(i){a.debug(`Failed to scan workspace projects: ${i}`);}let r=[t.python,t.poetry,t.pipx,t.go,t.rapidkitCore];if(t.healthScore=En(r,t.projects),t.rapidkitCore.status==="ok"){let i=t.rapidkitCore.message.match(/([\d.]+(?:rc\d+)?(?:a\d+)?(?:b\d+)?)/);i&&(t.coreVersion=i[1]);}return t}function B(e,o){let t=e.status==="ok"?"\u2705":e.status==="warn"?"\u26A0\uFE0F":"\u274C",r=e.status==="ok"?g.green:e.status==="warn"?g.yellow:g.red;console.log(`${t} ${g.bold(o)}: ${r(e.message)}`),e.paths&&e.paths.length>0?e.paths.forEach(i=>{let n=i.version?g.cyan(` -> ${i.version}`):"";console.log(` ${g.cyan("\u2022")} ${g.gray(i.location)}: ${g.dim(i.path)}${n}`);}):e.details&&console.log(` ${g.gray(e.details)}`);}function An(e){let o=e.issues.length>0,t=o?"\u26A0\uFE0F":"\u2705",r=o?g.yellow:g.green;if(console.log(`
|
|
4092
|
+
${t} ${g.bold("Project")}: ${r(e.name)}`),e.framework){let a=e.framework==="FastAPI"?"\u{1F40D}":e.framework==="NestJS"?"\u{1F985}":e.framework==="Go/Fiber"||e.framework==="Go/Gin"?"\u{1F439}":"\u{1F4E6}";console.log(` ${a} Framework: ${g.cyan(e.framework)}${e.kit?g.gray(` (${e.kit})`):""}`);}console.log(` ${g.gray(`Path: ${e.path}`)}`);let i=e.isGoProject===true,n=!i&&e.venvActive&&!e.coreInstalled;if(!i&&!n&&(e.venvActive?console.log(` \u2705 Virtual environment: ${g.green("Active")}`):console.log(` \u274C Virtual environment: ${g.red("Not found")}`),e.coreInstalled?console.log(` ${g.dim("\u2139")} RapidKit Core: ${g.gray(e.coreVersion||"In venv")} ${g.dim("(optional)")}`):console.log(` ${g.dim("\u2139")} RapidKit Core: ${g.gray("Using global installation")} ${g.dim("(recommended)")}`)),e.depsInstalled?console.log(` \u2705 Dependencies: ${g.green("Installed")}`):console.log(` \u26A0\uFE0F Dependencies: ${g.yellow("Not installed")}`),e.hasEnvFile!==void 0&&(e.hasEnvFile?console.log(` \u2705 Environment: ${g.green(".env configured")}`):console.log(` \u26A0\uFE0F Environment: ${g.yellow(".env missing")}`)),e.modulesHealthy!==void 0&&(e.modulesHealthy?console.log(` \u2705 Modules: ${g.green("Healthy")}`):e.missingModules&&e.missingModules.length>0&&console.log(` \u26A0\uFE0F Modules: ${g.yellow(`Missing ${e.missingModules.length} init file(s)`)}`)),e.stats){let a=[];e.stats.modules!==void 0&&a.push(`${e.stats.modules} module${e.stats.modules!==1?"s":""}`),a.length>0&&console.log(` \u{1F4CA} Stats: ${g.cyan(a.join(" \u2022 "))}`);}e.lastModified&&console.log(` \u{1F552} Last Modified: ${g.gray(e.lastModified)}`);let c=[];if(e.hasTests!==void 0&&c.push(e.hasTests?"\u2705 Tests":g.dim("\u2298 No tests")),e.hasDocker!==void 0&&c.push(e.hasDocker?"\u2705 Docker":g.dim("\u2298 No Docker")),e.hasCodeQuality!==void 0){let a=e.framework==="NestJS"?"ESLint":e.framework==="Go/Fiber"||e.framework==="Go/Gin"?"golangci-lint":"Ruff";c.push(e.hasCodeQuality?`\u2705 ${a}`:g.dim(`\u2298 No ${a}`));}c.length>0&&console.log(` ${c.join(" \u2022 ")}`),e.vulnerabilities!==void 0&&e.vulnerabilities>0&&console.log(` \u26A0\uFE0F Security: ${g.yellow(`${e.vulnerabilities} vulnerability(ies) found`)}`),e.issues.length>0&&(console.log(` ${g.bold("Issues:")}`),e.issues.forEach(a=>{console.log(` \u2022 ${g.yellow(a)}`);}),e.fixCommands&&e.fixCommands.length>0&&(console.log(`
|
|
4093
|
+
${g.bold.cyan("\u{1F527} Quick Fix:")}`),e.fixCommands.forEach(a=>{console.log(` ${g.cyan("$")} ${g.white(a)}`);})));}async function zt(e,o=false){let t=e.filter(i=>i.fixCommands&&i.fixCommands.length>0);if(t.length===0){console.log(g.green(`
|
|
4094
|
+
\u2705 No fixes needed - all projects are healthy!`));return}console.log(g.bold.cyan(`
|
|
859
4095
|
\u{1F527} Available Fixes:
|
|
860
|
-
`));for(let i of t)console.log(
|
|
861
|
-
\u26A0\uFE0F Fixes cancelled by user`));return}console.log(
|
|
4096
|
+
`));for(let i of t)console.log(g.bold(`Project: ${g.yellow(i.name)}`)),i.fixCommands.forEach((n,s)=>{console.log(` ${s+1}. ${g.cyan(n)}`);}),console.log();if(!o){console.log(g.gray("\u{1F4A1} Run with --fix flag to apply fixes automatically"));return}let{confirm:r}=await ae.prompt([{type:"confirm",name:"confirm",message:`Apply ${t.reduce((i,n)=>i+n.fixCommands.length,0)} fix(es)?`,default:false}]);if(!r){console.log(g.yellow(`
|
|
4097
|
+
\u26A0\uFE0F Fixes cancelled by user`));return}console.log(g.bold.cyan(`
|
|
862
4098
|
\u{1F680} Applying fixes...
|
|
863
|
-
`));for(let i of t){console.log(
|
|
864
|
-
`));}catch(s){console.log(
|
|
865
|
-
`));}}console.log(
|
|
866
|
-
\u2705 Fix process completed!`));}async function
|
|
4099
|
+
`));for(let i of t){console.log(g.bold(`Fixing ${g.cyan(i.name)}...`));for(let n of i.fixCommands)try{console.log(g.gray(` $ ${n}`)),await execa(n,{shell:true,stdio:"inherit"}),console.log(g.green(` \u2705 Success
|
|
4100
|
+
`));}catch(s){console.log(g.red(` \u274C Failed: ${s instanceof Error?s.message:String(s)}
|
|
4101
|
+
`));}}console.log(g.bold.green(`
|
|
4102
|
+
\u2705 Fix process completed!`));}async function ro(e={}){if(e.json||console.log(g.bold.cyan(`
|
|
867
4103
|
\u{1FA7A} RapidKit Health Check
|
|
868
|
-
`)),e.workspace){let o=await
|
|
869
|
-
\u{1F4CA} Health Score:`)),console.log(` ${c(`${s}%`)} ${
|
|
4104
|
+
`)),e.workspace){let o=await In(process.cwd());o||(a.error("No RapidKit workspace found in current directory or parents"),a.info('Run this command from within a workspace, or use "rapidkit doctor" for system check'),process.exit(1)),e.json||(console.log(g.bold(`Workspace: ${g.cyan(v.basename(o))}`)),console.log(g.gray(`Path: ${o}`)));let t=await Tn(o);if(e.json){let n={workspace:{name:v.basename(o),path:o},healthScore:t.healthScore,system:{python:t.python,poetry:t.poetry,pipx:t.pipx,rapidkitCore:t.rapidkitCore,versions:{core:t.coreVersion,npm:t.npmVersion}},projects:t.projects.map(s=>({name:s.name,path:s.path,venvActive:s.venvActive,depsInstalled:s.depsInstalled,coreInstalled:s.coreInstalled,coreVersion:s.coreVersion,issues:s.issues,fixCommands:s.fixCommands})),summary:{totalProjects:t.projects.length,totalIssues:t.projects.reduce((s,c)=>s+c.issues.length,0),hasSystemErrors:[t.python,t.rapidkitCore].some(s=>s.status==="error")}};console.log(JSON.stringify(n,null,2));return}if(t.healthScore){let n=t.healthScore,s=Math.round(n.passed/n.total*100),c=s>=80?g.green:s>=50?g.yellow:g.red,a="\u2588".repeat(Math.floor(s/5))+"\u2591".repeat(20-Math.floor(s/5));console.log(g.bold(`
|
|
4105
|
+
\u{1F4CA} Health Score:`)),console.log(` ${c(`${s}%`)} ${g.gray(a)}`),console.log(` ${g.green(`\u2705 ${n.passed} passed`)} ${g.gray("|")} ${g.yellow(`\u26A0\uFE0F ${n.warnings} warnings`)} ${g.gray("|")} ${g.red(`\u274C ${n.errors} errors`)}`);}if(console.log(g.bold(`
|
|
870
4106
|
|
|
871
4107
|
System Tools:
|
|
872
|
-
`)),
|
|
873
|
-
\u26A0\uFE0F Version mismatch: Core ${t.coreVersion} / CLI ${t.npmVersion}`)),console.log(
|
|
874
|
-
\u{1F4E6} Projects (${t.projects.length}):`)),t.projects.forEach(
|
|
875
|
-
\u{1F4E6} Projects:`)),console.log(
|
|
876
|
-
\u26A0\uFE0F Found ${
|
|
877
|
-
\u2705 All checks passed! Workspace is healthy.`));}else {console.log(
|
|
878
|
-
`));let o=await
|
|
879
|
-
\u274C Some required tools are missing`)),console.log(
|
|
880
|
-
Tip: Run "rapidkit doctor --workspace" from within a workspace for detailed project checks`))):(console.log(
|
|
881
|
-
\u2705 All required tools are installed!`)),console.log(
|
|
882
|
-
Tip: Run "rapidkit doctor --workspace" from within a workspace for detailed project checks`)));}console.log("");}var
|
|
4108
|
+
`)),B(t.python,"Python"),B(t.poetry,"Poetry"),B(t.pipx,"pipx"),B(t.go,"Go"),B(t.rapidkitCore,"RapidKit Core"),t.coreVersion&&t.npmVersion){let n=t.coreVersion.split(".")[1],s=t.npmVersion.split(".")[1];n!==s&&(console.log(g.yellow(`
|
|
4109
|
+
\u26A0\uFE0F Version mismatch: Core ${t.coreVersion} / CLI ${t.npmVersion}`)),console.log(g.gray(" Consider updating to matching versions for best compatibility")));}t.projects.length>0?(console.log(g.bold(`
|
|
4110
|
+
\u{1F4E6} Projects (${t.projects.length}):`)),t.projects.forEach(n=>An(n))):(console.log(g.bold(`
|
|
4111
|
+
\u{1F4E6} Projects:`)),console.log(g.gray(" No RapidKit projects found in workspace")));let r=t.projects.reduce((n,s)=>n+s.issues.length,0),i=[t.python,t.rapidkitCore].some(n=>n.status==="error");i||r>0?(console.log(g.bold.yellow(`
|
|
4112
|
+
\u26A0\uFE0F Found ${r} project issue(s)`)),i&&console.log(g.bold.red("\u274C System requirements not met")),e.fix?await zt(t.projects,true):r>0&&await zt(t.projects,false)):console.log(g.bold.green(`
|
|
4113
|
+
\u2705 All checks passed! Workspace is healthy.`));}else {console.log(g.bold(`System Tools:
|
|
4114
|
+
`));let o=await Yt(),t=await Qt(),r=await Xt(),i=await Zt(),n=await eo();B(o,"Python"),B(t,"Poetry"),B(r,"pipx"),B(i,"Go"),B(n,"RapidKit Core"),[o,n].some(c=>c.status==="error")?(console.log(g.bold.red(`
|
|
4115
|
+
\u274C Some required tools are missing`)),console.log(g.gray(`
|
|
4116
|
+
Tip: Run "rapidkit doctor --workspace" from within a workspace for detailed project checks`))):(console.log(g.bold.green(`
|
|
4117
|
+
\u2705 All required tools are installed!`)),console.log(g.gray(`
|
|
4118
|
+
Tip: Run "rapidkit doctor --workspace" from within a workspace for detailed project checks`)));}console.log("");}var ht=v.join(qo__default.homedir(),".rapidkit"),He=v.join(ht,"config.json");function ue(){try{if(!A.existsSync(He))return {};let e=A.readFileSync(He,"utf-8");return JSON.parse(e)}catch{return {}}}function Ke(e){let t={...ue(),...e};A.existsSync(ht)||A.mkdirSync(ht,{recursive:true}),A.writeFileSync(He,JSON.stringify(t,null,2),"utf-8");}function xe(){return process.env.OPENAI_API_KEY||ue().openaiApiKey||null}function wt(){return ue().aiEnabled!==false}function yt(){return He}async function io(){return (await import('inquirer')).default}function so(e){let o=e.command("config").description("Configure RapidKit settings");o.command("set-api-key").description("Set OpenAI API key for AI features").option("--key <key>","API key (or enter interactively)").action(async t=>{let r=t.key;r?r.startsWith("sk-")||(console.log(g.red(`
|
|
883
4119
|
\u274C Invalid API key format (should start with sk-)
|
|
884
|
-
`)),process.exit(1)):
|
|
4120
|
+
`)),process.exit(1)):r=(await(await io()).prompt([{type:"password",name:"apiKey",message:"Enter your OpenAI API key:",validate:s=>s?s.startsWith("sk-")?s.length<20?"API key seems too short":true:"Invalid API key format (should start with sk-)":"API key is required"}])).apiKey,Ke({openaiApiKey:r}),console.log(g.green(`
|
|
885
4121
|
\u2705 OpenAI API key saved successfully!
|
|
886
|
-
`)),console.log(
|
|
887
|
-
\u{1F389} You can now use AI features:`)),console.log(
|
|
888
|
-
\u{1F4A1} To generate module embeddings (one-time):`)),console.log(
|
|
889
|
-
`));}),o.command("show").description("Show current configuration").action(()=>{let t=
|
|
4122
|
+
`)),console.log(g.gray(`Stored in: ${yt()}`)),console.log(g.cyan(`
|
|
4123
|
+
\u{1F389} You can now use AI features:`)),console.log(g.white(' rapidkit ai recommend "I need user authentication"')),console.log(g.gray(`
|
|
4124
|
+
\u{1F4A1} To generate module embeddings (one-time):`)),console.log(g.white(" cd rapidkit-npm")),console.log(g.white(` npx tsx src/ai/generate-embeddings.ts
|
|
4125
|
+
`));}),o.command("show").description("Show current configuration").action(()=>{let t=ue();if(console.log(g.bold(`
|
|
890
4126
|
\u2699\uFE0F RapidKit Configuration
|
|
891
|
-
`)),t.openaiApiKey){let
|
|
892
|
-
\u{1F4C1} Config file: ${
|
|
893
|
-
`));}),o.command("remove-api-key").description("Remove stored OpenAI API key").action(async()=>{if(!
|
|
4127
|
+
`)),t.openaiApiKey){let r=t.openaiApiKey.substring(0,8)+"..."+t.openaiApiKey.slice(-4);console.log(g.cyan("OpenAI API Key:"),g.white(r));}else console.log(g.cyan("OpenAI API Key:"),g.red("Not set")),console.log(g.gray(" Set with: rapidkit config set-api-key"));console.log(g.cyan("AI Features:"),t.aiEnabled!==false?g.green("Enabled"):g.red("Disabled")),console.log(g.gray(`
|
|
4128
|
+
\u{1F4C1} Config file: ${yt()}
|
|
4129
|
+
`));}),o.command("remove-api-key").description("Remove stored OpenAI API key").action(async()=>{if(!ue().openaiApiKey){console.log(g.yellow(`
|
|
894
4130
|
\u26A0\uFE0F No API key is currently stored
|
|
895
|
-
`));return}(await(await
|
|
4131
|
+
`));return}(await(await io()).prompt([{type:"confirm",name:"confirm",message:"Are you sure you want to remove your OpenAI API key?",default:false}])).confirm?(Ke({openaiApiKey:void 0}),console.log(g.green(`
|
|
896
4132
|
\u2705 API key removed successfully
|
|
897
|
-
`))):console.log(
|
|
4133
|
+
`))):console.log(g.gray(`
|
|
898
4134
|
Cancelled
|
|
899
|
-
`));}),o.command("ai <action>").description("Enable or disable AI features (enable|disable)").action(t=>{t!=="enable"&&t!=="disable"&&(console.log(
|
|
900
|
-
\u274C Invalid action: ${t}`)),console.log(
|
|
901
|
-
`)),process.exit(1));let
|
|
902
|
-
\u2705 AI features ${
|
|
903
|
-
`));});}var je=null,Ie=false,Ve=null;async function jo(){return Ve||(Ve=(await import('openai')).default),Ve}function Pt(){Ie=true;}function jt(e){let t=new Array(1536),n=0;for(let r=0;r<e.length;r++)n=(n<<5)-n+e.charCodeAt(r),n=n&n;for(let r=0;r<1536;r++)n=n*1664525+1013904223&4294967295,t[r]=n/4294967295*2-1;let i=Math.sqrt(t.reduce((r,s)=>r+s*s,0));return t.map(r=>r/i)}async function Ce(e){let o=await jo();je=new o({apiKey:e});}function It(){if(!je)throw new Error("OpenAI client not initialized. Call initOpenAI() first with your API key.");return je}async function Ct(e){return Ie?jt(e):(await It().embeddings.create({model:"text-embedding-3-small",input:e,encoding_format:"float"})).data[0].embedding}async function Rt(e){return Ie?e.map(jt):(await It().embeddings.create({model:"text-embedding-3-small",input:e,encoding_format:"float"})).data.map(n=>n.embedding)}function $t(){return je!==null}function At(){return Ie}var Ro=promisify(exec),Et=[{id:"authentication-core",name:"Authentication Core",category:"auth",description:"Complete authentication system with password hashing, JWT tokens, OAuth 2.0, and secure session management",longDescription:"Production-ready authentication with bcrypt password hashing, JWT access/refresh tokens, OAuth 2.0 providers (Google, GitHub, etc), rate limiting, and security best practices.",keywords:["auth","login","password","jwt","oauth","token","authentication","security","signin","signup"],framework:"both",dependencies:[],useCases:["User login and logout","Password reset flow","OAuth social login (Google, GitHub)","JWT authentication","Secure session management","Token refresh","Rate limiting"]},{id:"users-core",name:"Users Core",category:"auth",description:"User management system with profiles, roles, permissions, and user CRUD operations",longDescription:"Complete user management with user profiles, role-based access control (RBAC), permissions, user search, soft delete, and audit trails.",keywords:["user","profile","role","permission","rbac","management","admin","accounts"],framework:"both",dependencies:["authentication-core"],useCases:["User registration","User profile management","Role management (admin, user, etc)","Permission system","User administration dashboard","Soft delete users"]},{id:"session-management",name:"Session Management",category:"auth",description:"Secure session handling with Redis storage, session rotation, and device tracking",longDescription:"Advanced session management with Redis-backed storage, automatic session rotation, device fingerprinting, IP tracking, and session revocation.",keywords:["session","redis","cookie","storage","device","tracking"],framework:"both",dependencies:["authentication-core","redis-cache"],useCases:["User session management","Remember me functionality","Device tracking","Session security","Logout from all devices","Session expiration"]},{id:"db-postgres",name:"PostgreSQL",category:"database",description:"PostgreSQL integration with async SQLAlchemy, migrations, connection pooling, and query optimization",longDescription:"Production-ready PostgreSQL with async SQLAlchemy 2.0, Alembic migrations, connection pooling, query optimization, JSON support, and full-text search.",keywords:["postgres","postgresql","database","sql","sqlalchemy","migration","orm","relational"],framework:"both",dependencies:[],useCases:["Relational database","Complex SQL queries","Database transactions","Data integrity","Production-grade database","ACID compliance"]},{id:"db-mongodb",name:"MongoDB",category:"database",description:"MongoDB integration with Motor async driver, schema validation, and aggregation pipelines",longDescription:"Async MongoDB with Motor driver, Pydantic schema validation, aggregation pipelines, indexes, and Atlas integration.",keywords:["mongodb","mongo","nosql","document","database","motor"],framework:"both",dependencies:[],useCases:["Document storage","Flexible schema","Real-time data","JSON documents","Unstructured data","Analytics"]},{id:"stripe-payment",name:"Stripe Payment",category:"payment",description:"Stripe integration with payment intents, subscriptions, webhooks, and customer portal",longDescription:"Complete Stripe integration with Payment Intents API, subscription management, automatic webhooks, customer portal, refunds, and SCA compliance.",keywords:["stripe","payment","subscription","billing","checkout","webhook","credit card"],framework:"both",dependencies:[],useCases:["Accept credit card payments","Subscription billing","One-time payments","Checkout flow","Payment webhooks","Refunds and disputes"]},{id:"email",name:"Email",category:"communication",description:"Email sending with templates, SMTP/SendGrid/AWS SES support, and queue management",longDescription:"Production email system with Jinja2 templates, multiple providers (SMTP, SendGrid, AWS SES), queue management, retry logic, and bounce handling.",keywords:["email","mail","smtp","sendgrid","ses","template","notification"],framework:"both",dependencies:[],useCases:["Welcome emails","Password reset emails","Notifications","Marketing emails","Transactional emails","Email templates"]},{id:"sms",name:"SMS",category:"communication",description:"SMS sending with Twilio, verification codes, and delivery tracking",longDescription:"SMS integration with Twilio, verification codes, two-factor authentication, delivery tracking, and international support.",keywords:["sms","twilio","text","message","2fa","verification","otp"],framework:"both",dependencies:[],useCases:["2FA verification codes","SMS notifications","Phone verification","OTP generation","SMS alerts"]},{id:"redis-cache",name:"Redis Cache",category:"infrastructure",description:"Redis caching with decorators, TTL management, and cache invalidation patterns",longDescription:"Redis integration with async client, caching decorators, TTL management, cache invalidation, pub/sub, and rate limiting.",keywords:["redis","cache","memory","performance","speed","pubsub"],framework:"both",dependencies:[],useCases:["API response caching","Session storage","Rate limiting","Real-time features","Performance optimization","Pub/sub messaging"]},{id:"celery",name:"Celery",category:"infrastructure",description:"Background task processing with Celery, periodic tasks, and monitoring",longDescription:"Celery task queue with Redis/RabbitMQ backend, periodic tasks (cron), task monitoring, retry logic, and failure handling.",keywords:["celery","task","background","queue","async","worker","job","cron"],framework:"fastapi",dependencies:["redis-cache"],useCases:["Background email sending","Data processing","Report generation","Scheduled tasks","Long-running jobs"]},{id:"storage",name:"Storage",category:"infrastructure",description:"File storage with S3, local filesystem, and image processing",longDescription:"Unified storage interface for AWS S3, local files, image resizing, format conversion, CDN integration, and presigned URLs.",keywords:["storage","s3","file","upload","image","cdn","aws"],framework:"both",dependencies:[],useCases:["File uploads","Image storage","Document management","Profile pictures","Media files","CDN integration"]}],ne=null,_t=0,$o=300*1e3;function Ao(e){return {id:e.name||e.id||e.module_id||"",name:e.display_name||e.name||"",category:_o(e.category||"infrastructure"),description:e.description||e.summary||"",longDescription:e.long_description||e.description||"",keywords:e.keywords||e.tags||[],framework:Eo(e.framework),dependencies:e.dependencies||[],useCases:e.use_cases||e.useCases||[]}}function _o(e){return {auth:"auth",authentication:"auth",database:"database",payment:"payment",billing:"payment",communication:"communication",infrastructure:"infrastructure",security:"security",analytics:"analytics"}[e.toLowerCase()]||"infrastructure"}function Eo(e){if(!e)return "both";if(typeof e=="string"){if(e.toLowerCase().includes("fastapi"))return "fastapi";if(e.toLowerCase().includes("nest"))return "nestjs"}return "both"}async function So(){try{let{stdout:e}=await Ro("rapidkit modules list --json-schema 1",{timeout:1e4,maxBuffer:10485760}),o=e.match(/\{[\s\S]*\}/),t=o?o[0]:e,n=JSON.parse(t),i=[];return Array.isArray(n)?i=n:n.modules&&Array.isArray(n.modules)?i=n.modules:n.data&&Array.isArray(n.data)&&(i=n.data),i.map(Ao).filter(r=>r.id&&r.name)}catch(e){return e.code==="ENOENT"?console.warn("\u26A0\uFE0F RapidKit Python Core not found in PATH"):e.killed?console.warn("\u26A0\uFE0F Python Core command timed out"):console.warn("\u26A0\uFE0F Failed to fetch modules from Python Core:",e.message),console.warn(" Using fallback module catalog (11 modules)"),Et}}async function Re(){let e=Date.now();return ne&&e-_t<$o||(ne=await So(),_t=e,ne.length===0&&(console.warn("\u26A0\uFE0F No modules found, using fallback catalog"),ne=Et)),ne}var Do=fileURLToPath(import.meta.url),Mt=w.dirname(Do),de=null;function Ko(){if(de)return de;let e=[w.join(Mt,"../../data/modules-embeddings.json"),w.join(Mt,"../data/modules-embeddings.json"),w.join(process.cwd(),"data/modules-embeddings.json")],o=null;for(let i of e)if(S.existsSync(i)){o=i;break}if(!o)throw new Error("embeddings file not found");let t=S.readFileSync(o,"utf-8"),n=JSON.parse(t);return Array.isArray(n)?de={model:"mock-or-text-embedding-3-small",dimension:n[0]?.embedding?.length||1536,generated_at:new Date().toISOString(),modules:n}:de=n,de}function Fo(e,o){if(e.length!==o.length)throw new Error("Vectors must have the same length");let t=0,n=0,i=0;for(let s=0;s<e.length;s++)t+=e[s]*o[s],n+=e[s]*e[s],i+=o[s]*o[s];let r=Math.sqrt(n)*Math.sqrt(i);return r===0?0:t/r}function Oo(e,o){let t=o.toLowerCase(),n=e.keywords.filter(i=>t.includes(i)||i.includes(t));return n.length>0?`Matches: ${n.slice(0,3).join(", ")}`:`Relevant for: ${e.useCases[0]}`}async function Dt(e,o=5){let t=Ko(),n=await Re(),i=await Ct(e),r=t.modules.map(s=>{let c=n.find(l=>l.id===s.id);if(!c)return null;let a=Fo(i,s.embedding);return {module:c,score:a,reason:Oo(c,e)}}).filter(s=>s!==null);return r.sort((s,c)=>c.score-s.score),r.slice(0,o)}var Wo=fileURLToPath(import.meta.url),Kt=w.dirname(Wo);async function Ft(){return (await import('inquirer')).default}function Lo(){return [w.join(Kt,"../../data/modules-embeddings.json"),w.join(Kt,"../data/modules-embeddings.json"),w.join(process.cwd(),"data/modules-embeddings.json")]}function Ot(){let e=Lo();for(let o of e)if(S.existsSync(o))try{let t=JSON.parse(S.readFileSync(o,"utf-8")),n=Array.isArray(t)?t:t.modules||[];return {exists:true,path:o,moduleCount:n.length,generatedAt:t.generated_at||null}}catch{continue}return {exists:false,path:null,moduleCount:0,generatedAt:null}}async function Ae(e=true,o){try{if(!$t()&&!At())return console.log(u.red(`
|
|
904
|
-
\u274C OpenAI not initialized`)),console.log(
|
|
905
|
-
`)),false;console.log(
|
|
4135
|
+
`));}),o.command("ai <action>").description("Enable or disable AI features (enable|disable)").action(t=>{t!=="enable"&&t!=="disable"&&(console.log(g.red(`
|
|
4136
|
+
\u274C Invalid action: ${t}`)),console.log(g.gray(`Use: rapidkit config ai enable|disable
|
|
4137
|
+
`)),process.exit(1));let r=t==="enable";Ke({aiEnabled:r}),console.log(g.green(`
|
|
4138
|
+
\u2705 AI features ${r?"enabled":"disabled"}
|
|
4139
|
+
`));});}var Ue=null,Ve=false,vt=null;async function Nn(){return vt||(vt=(await import('openai')).default),vt}function ao(){Ve=true;}function co(e){let t=new Array(1536),r=0;for(let n=0;n<e.length;n++)r=(r<<5)-r+e.charCodeAt(n),r=r&r;for(let n=0;n<1536;n++)r=r*1664525+1013904223&4294967295,t[n]=r/4294967295*2-1;let i=Math.sqrt(t.reduce((n,s)=>n+s*s,0));return t.map(n=>n/i)}async function We(e){let o=await Nn();Ue=new o({apiKey:e});}function lo(){if(!Ue)throw new Error("OpenAI client not initialized. Call initOpenAI() first with your API key.");return Ue}async function po(e){return Ve?co(e):(await lo().embeddings.create({model:"text-embedding-3-small",input:e,encoding_format:"float"})).data[0].embedding}async function uo(e){return Ve?e.map(co):(await lo().embeddings.create({model:"text-embedding-3-small",input:e,encoding_format:"float"})).data.map(r=>r.embedding)}function go(){return Ue!==null}function mo(){return Ve}var $n=promisify(exec),ho=[{id:"authentication-core",name:"Authentication Core",category:"auth",description:"Complete authentication system with password hashing, JWT tokens, OAuth 2.0, and secure session management",longDescription:"Production-ready authentication with bcrypt password hashing, JWT access/refresh tokens, OAuth 2.0 providers (Google, GitHub, etc), rate limiting, and security best practices.",keywords:["auth","login","password","jwt","oauth","token","authentication","security","signin","signup"],framework:"both",dependencies:[],useCases:["User login and logout","Password reset flow","OAuth social login (Google, GitHub)","JWT authentication","Secure session management","Token refresh","Rate limiting"]},{id:"users-core",name:"Users Core",category:"auth",description:"User management system with profiles, roles, permissions, and user CRUD operations",longDescription:"Complete user management with user profiles, role-based access control (RBAC), permissions, user search, soft delete, and audit trails.",keywords:["user","profile","role","permission","rbac","management","admin","accounts"],framework:"both",dependencies:["authentication-core"],useCases:["User registration","User profile management","Role management (admin, user, etc)","Permission system","User administration dashboard","Soft delete users"]},{id:"session-management",name:"Session Management",category:"auth",description:"Secure session handling with Redis storage, session rotation, and device tracking",longDescription:"Advanced session management with Redis-backed storage, automatic session rotation, device fingerprinting, IP tracking, and session revocation.",keywords:["session","redis","cookie","storage","device","tracking"],framework:"both",dependencies:["authentication-core","redis-cache"],useCases:["User session management","Remember me functionality","Device tracking","Session security","Logout from all devices","Session expiration"]},{id:"db-postgres",name:"PostgreSQL",category:"database",description:"PostgreSQL integration with async SQLAlchemy, migrations, connection pooling, and query optimization",longDescription:"Production-ready PostgreSQL with async SQLAlchemy 2.0, Alembic migrations, connection pooling, query optimization, JSON support, and full-text search.",keywords:["postgres","postgresql","database","sql","sqlalchemy","migration","orm","relational"],framework:"both",dependencies:[],useCases:["Relational database","Complex SQL queries","Database transactions","Data integrity","Production-grade database","ACID compliance"]},{id:"db-mongodb",name:"MongoDB",category:"database",description:"MongoDB integration with Motor async driver, schema validation, and aggregation pipelines",longDescription:"Async MongoDB with Motor driver, Pydantic schema validation, aggregation pipelines, indexes, and Atlas integration.",keywords:["mongodb","mongo","nosql","document","database","motor"],framework:"both",dependencies:[],useCases:["Document storage","Flexible schema","Real-time data","JSON documents","Unstructured data","Analytics"]},{id:"stripe-payment",name:"Stripe Payment",category:"payment",description:"Stripe integration with payment intents, subscriptions, webhooks, and customer portal",longDescription:"Complete Stripe integration with Payment Intents API, subscription management, automatic webhooks, customer portal, refunds, and SCA compliance.",keywords:["stripe","payment","subscription","billing","checkout","webhook","credit card"],framework:"both",dependencies:[],useCases:["Accept credit card payments","Subscription billing","One-time payments","Checkout flow","Payment webhooks","Refunds and disputes"]},{id:"email",name:"Email",category:"communication",description:"Email sending with templates, SMTP/SendGrid/AWS SES support, and queue management",longDescription:"Production email system with Jinja2 templates, multiple providers (SMTP, SendGrid, AWS SES), queue management, retry logic, and bounce handling.",keywords:["email","mail","smtp","sendgrid","ses","template","notification"],framework:"both",dependencies:[],useCases:["Welcome emails","Password reset emails","Notifications","Marketing emails","Transactional emails","Email templates"]},{id:"sms",name:"SMS",category:"communication",description:"SMS sending with Twilio, verification codes, and delivery tracking",longDescription:"SMS integration with Twilio, verification codes, two-factor authentication, delivery tracking, and international support.",keywords:["sms","twilio","text","message","2fa","verification","otp"],framework:"both",dependencies:[],useCases:["2FA verification codes","SMS notifications","Phone verification","OTP generation","SMS alerts"]},{id:"redis-cache",name:"Redis Cache",category:"infrastructure",description:"Redis caching with decorators, TTL management, and cache invalidation patterns",longDescription:"Redis integration with async client, caching decorators, TTL management, cache invalidation, pub/sub, and rate limiting.",keywords:["redis","cache","memory","performance","speed","pubsub"],framework:"both",dependencies:[],useCases:["API response caching","Session storage","Rate limiting","Real-time features","Performance optimization","Pub/sub messaging"]},{id:"celery",name:"Celery",category:"infrastructure",description:"Background task processing with Celery, periodic tasks, and monitoring",longDescription:"Celery task queue with Redis/RabbitMQ backend, periodic tasks (cron), task monitoring, retry logic, and failure handling.",keywords:["celery","task","background","queue","async","worker","job","cron"],framework:"fastapi",dependencies:["redis-cache"],useCases:["Background email sending","Data processing","Report generation","Scheduled tasks","Long-running jobs"]},{id:"storage",name:"Storage",category:"infrastructure",description:"File storage with S3, local filesystem, and image processing",longDescription:"Unified storage interface for AWS S3, local files, image resizing, format conversion, CDN integration, and presigned URLs.",keywords:["storage","s3","file","upload","image","cdn","aws"],framework:"both",dependencies:[],useCases:["File uploads","Image storage","Document management","Profile pictures","Media files","CDN integration"]}],ge=null,fo=0,Dn=300*1e3;function Mn(e){return {id:e.name||e.id||e.module_id||"",name:e.display_name||e.name||"",category:qn(e.category||"infrastructure"),description:e.description||e.summary||"",longDescription:e.long_description||e.description||"",keywords:e.keywords||e.tags||[],framework:Ln(e.framework),dependencies:e.dependencies||[],useCases:e.use_cases||e.useCases||[]}}function qn(e){return {auth:"auth",authentication:"auth",database:"database",payment:"payment",billing:"payment",communication:"communication",infrastructure:"infrastructure",security:"security",analytics:"analytics"}[e.toLowerCase()]||"infrastructure"}function Ln(e){if(!e)return "both";if(typeof e=="string"){if(e.toLowerCase().includes("fastapi"))return "fastapi";if(e.toLowerCase().includes("nest"))return "nestjs"}return "both"}async function Fn(){try{let{stdout:e}=await $n("rapidkit modules list --json-schema 1",{timeout:1e4,maxBuffer:10485760}),o=e.match(/\{[\s\S]*\}/),t=o?o[0]:e,r=JSON.parse(t),i=[];return Array.isArray(r)?i=r:r.modules&&Array.isArray(r.modules)?i=r.modules:r.data&&Array.isArray(r.data)&&(i=r.data),i.map(Mn).filter(n=>n.id&&n.name)}catch(e){return e.code==="ENOENT"?console.warn("\u26A0\uFE0F RapidKit Python Core not found in PATH"):e.killed?console.warn("\u26A0\uFE0F Python Core command timed out"):console.warn("\u26A0\uFE0F Failed to fetch modules from Python Core:",e.message),console.warn(" Using fallback module catalog (11 modules)"),ho}}async function Be(){let e=Date.now();return ge&&e-fo<Dn||(ge=await Fn(),fo=e,ge.length===0&&(console.warn("\u26A0\uFE0F No modules found, using fallback catalog"),ge=ho)),ge}var Kn=fileURLToPath(import.meta.url),yo=v.dirname(Kn),Ce=null;function Un(){if(Ce)return Ce;let e=[v.join(yo,"../../data/modules-embeddings.json"),v.join(yo,"../data/modules-embeddings.json"),v.join(process.cwd(),"data/modules-embeddings.json")],o=null;for(let i of e)if(A.existsSync(i)){o=i;break}if(!o)throw new Error("embeddings file not found");let t=A.readFileSync(o,"utf-8"),r=JSON.parse(t);return Array.isArray(r)?Ce={model:"mock-or-text-embedding-3-small",dimension:r[0]?.embedding?.length||1536,generated_at:new Date().toISOString(),modules:r}:Ce=r,Ce}function Vn(e,o){if(e.length!==o.length)throw new Error("Vectors must have the same length");let t=0,r=0,i=0;for(let s=0;s<e.length;s++)t+=e[s]*o[s],r+=e[s]*e[s],i+=o[s]*o[s];let n=Math.sqrt(r)*Math.sqrt(i);return n===0?0:t/n}function Wn(e,o){let t=o.toLowerCase(),r=e.keywords.filter(i=>t.includes(i)||i.includes(t));return r.length>0?`Matches: ${r.slice(0,3).join(", ")}`:`Relevant for: ${e.useCases[0]}`}async function vo(e,o=5){let t=Un(),r=await Be(),i=await po(e),n=t.modules.map(s=>{let c=r.find(l=>l.id===s.id);if(!c)return null;let a=Vn(i,s.embedding);return {module:c,score:a,reason:Wn(c,e)}}).filter(s=>s!==null);return n.sort((s,c)=>c.score-s.score),n.slice(0,o)}var zn=fileURLToPath(import.meta.url),bo=v.dirname(zn);async function ko(){return (await import('inquirer')).default}function Yn(){return [v.join(bo,"../../data/modules-embeddings.json"),v.join(bo,"../data/modules-embeddings.json"),v.join(process.cwd(),"data/modules-embeddings.json")]}function Ro(){let e=Yn();for(let o of e)if(A.existsSync(o))try{let t=JSON.parse(A.readFileSync(o,"utf-8")),r=Array.isArray(t)?t:t.modules||[];return {exists:true,path:o,moduleCount:r.length,generatedAt:t.generated_at||null}}catch{continue}return {exists:false,path:null,moduleCount:0,generatedAt:null}}async function ze(e=true,o){try{if(!go()&&!mo())return console.log(g.red(`
|
|
4140
|
+
\u274C OpenAI not initialized`)),console.log(g.yellow("Please set your API key:")),console.log(g.white(" rapidkit config set-api-key")),console.log(g.gray(` OR set: export OPENAI_API_KEY="sk-..."
|
|
4141
|
+
`)),false;console.log(g.blue(`
|
|
906
4142
|
\u{1F916} Generating AI embeddings for RapidKit modules...
|
|
907
|
-
`)),console.log(
|
|
908
|
-
`));let
|
|
909
|
-
`)),e){let s=await
|
|
4143
|
+
`)),console.log(g.gray("\u{1F4E1} Fetching modules from RapidKit..."));let t=await Be();console.log(g.green(`\u2713 Found ${t.length} modules
|
|
4144
|
+
`));let r=t.length*50/1e6*.02;if(console.log(g.cyan(`\u{1F4B0} Estimated cost: ~$${r.toFixed(3)}`)),console.log(g.gray(` (Based on ${t.length} modules at $0.02/1M tokens)
|
|
4145
|
+
`)),e){let s=await ko(),{confirm:c}=await s.prompt([{type:"confirm",name:"confirm",message:"Generate embeddings now?",default:true}]);if(!c)return console.log(g.yellow(`
|
|
910
4146
|
\u26A0\uFE0F Embeddings generation cancelled
|
|
911
|
-
`)),false}let i=t.map(s=>`${s.name}. ${s.description}. ${s.longDescription}. Keywords: ${s.keywords.join(", ")}. Use cases: ${s.useCases.join(", ")}.`),
|
|
912
|
-
\u2705 Embeddings generated successfully!`)),console.log(
|
|
913
|
-
`)),true}catch(s){return
|
|
914
|
-
\u274C OpenAI API quota exceeded`)),console.log(
|
|
915
|
-
`))):s.message?.includes("401")?(console.log(
|
|
916
|
-
\u274C Invalid API key`)),console.log(
|
|
917
|
-
`))):console.log(
|
|
4147
|
+
`)),false}let i=t.map(s=>`${s.name}. ${s.description}. ${s.longDescription}. Keywords: ${s.keywords.join(", ")}. Use cases: ${s.useCases.join(", ")}.`),n=pt(`Generating embeddings for ${t.length} modules...`).start();try{let s=await uo(i);n.succeed(`Generated embeddings for ${t.length} modules`);let c={model:"text-embedding-3-small",dimension:s[0].length,generated_at:new Date().toISOString(),modules:t.map((d,p)=>({id:d.id,name:d.name,embedding:s[p]}))},a=o||v.join(process.cwd(),"data","modules-embeddings.json"),l=v.dirname(a);return A.existsSync(l)||A.mkdirSync(l,{recursive:true}),A.writeFileSync(a,JSON.stringify(c,null,2)),console.log(g.green(`
|
|
4148
|
+
\u2705 Embeddings generated successfully!`)),console.log(g.gray(`\u{1F4C1} Saved to: ${a}`)),console.log(g.gray(`\u{1F4CA} Size: ${t.length} modules, ${s[0].length} dimensions
|
|
4149
|
+
`)),true}catch(s){return n.fail("Failed to generate embeddings"),s.message?.includes("429")?(console.log(g.red(`
|
|
4150
|
+
\u274C OpenAI API quota exceeded`)),console.log(g.yellow(`Please check your billing: https://platform.openai.com/account/billing
|
|
4151
|
+
`))):s.message?.includes("401")?(console.log(g.red(`
|
|
4152
|
+
\u274C Invalid API key`)),console.log(g.yellow("Please set a valid API key:")),console.log(g.white(` rapidkit config set-api-key
|
|
4153
|
+
`))):console.log(g.red(`
|
|
918
4154
|
\u274C Error: ${s.message}
|
|
919
|
-
`)),false}}catch(t){return console.log(
|
|
4155
|
+
`)),false}}catch(t){return console.log(g.red(`
|
|
920
4156
|
\u274C Failed to generate embeddings: ${t.message}
|
|
921
|
-
`)),false}}async function
|
|
922
|
-
\u26A0\uFE0F Module embeddings not found`)),console.log(
|
|
923
|
-
`)),!e)return console.log(
|
|
924
|
-
`)),false;let t=await
|
|
4157
|
+
`)),false}}async function xo(e=true){if(Ro().exists)return true;if(console.log(g.yellow(`
|
|
4158
|
+
\u26A0\uFE0F Module embeddings not found`)),console.log(g.gray(`AI recommendations require embeddings to be generated.
|
|
4159
|
+
`)),!e)return console.log(g.red("\u274C Cannot generate embeddings in non-interactive mode")),console.log(g.white(`Run: rapidkit ai generate-embeddings
|
|
4160
|
+
`)),false;let t=await ko(),{action:r}=await t.prompt([{type:"list",name:"action",message:"What would you like to do?",choices:[{name:"\u{1F680} Generate embeddings now (requires OpenAI API key)",value:"generate"},{name:"\u{1F4DD} Show me how to generate them manually",value:"manual"},{name:"\u274C Cancel",value:"cancel"}]}]);return r==="generate"?await ze(true):(r==="manual"&&(console.log(g.cyan(`
|
|
925
4161
|
\u{1F4DD} To generate embeddings manually:
|
|
926
|
-
`)),console.log(
|
|
927
|
-
`)),console.log(
|
|
928
|
-
`)),console.log(
|
|
929
|
-
`))),false)}async function
|
|
930
|
-
\u{1F504} Updating embeddings...`)),console.log(
|
|
931
|
-
`)),await
|
|
932
|
-
\u26A0\uFE0F No existing embeddings found`)),console.log(
|
|
933
|
-
`)),false)}async function
|
|
934
|
-
\u26A0\uFE0F AI features are disabled`)),console.log(
|
|
935
|
-
`)),process.exit(1));let i=
|
|
4162
|
+
`)),console.log(g.white("1. Get OpenAI API key from: https://platform.openai.com/api-keys")),console.log(g.white("2. Set the API key:")),console.log(g.gray(" rapidkit config set-api-key")),console.log(g.gray(` OR: export OPENAI_API_KEY="sk-..."
|
|
4163
|
+
`)),console.log(g.white("3. Generate embeddings:")),console.log(g.gray(` rapidkit ai generate-embeddings
|
|
4164
|
+
`)),console.log(g.cyan(`\u{1F4B0} Cost: ~$0.50 one-time
|
|
4165
|
+
`))),false)}async function Co(){let e=Ro();return e.exists?(console.log(g.blue(`
|
|
4166
|
+
\u{1F504} Updating embeddings...`)),console.log(g.gray(`Current: ${e.moduleCount} modules`)),console.log(g.gray(`Generated: ${e.generatedAt||"unknown"}
|
|
4167
|
+
`)),await ze(true,e.path)):(console.log(g.yellow(`
|
|
4168
|
+
\u26A0\uFE0F No existing embeddings found`)),console.log(g.gray(`Use: rapidkit ai generate-embeddings
|
|
4169
|
+
`)),false)}async function So(){return (await import('inquirer')).default}function _o(e){let o=e.command("ai").description("AI-powered features");o.command("recommend").description("Get AI-powered module recommendations").argument("[query]",'What do you want to build? (e.g., "user authentication with email")').option("-n, --number <count>","Number of recommendations","5").option("--json","Output as JSON").action(async(t,r)=>{try{wt()||(console.log(g.yellow(`
|
|
4170
|
+
\u26A0\uFE0F AI features are disabled`)),console.log(g.gray(`Enable with: rapidkit config ai enable
|
|
4171
|
+
`)),process.exit(1));let i=xe();i?await We(i):(console.log(g.yellow(`
|
|
936
4172
|
\u26A0\uFE0F OpenAI API key not configured - using MOCK MODE for testing
|
|
937
|
-
`)),console.log(
|
|
938
|
-
`)),console.log(
|
|
939
|
-
`)),
|
|
4173
|
+
`)),console.log(g.gray("\u{1F4DD} Note: Mock embeddings provide approximate results for testing.")),console.log(g.gray(` For production, configure your OpenAI API key:
|
|
4174
|
+
`)),console.log(g.white(" 1. Get your key from: https://platform.openai.com/api-keys")),console.log(g.white(" 2. Configure it: rapidkit config set-api-key")),console.log(g.gray(` OR set: export OPENAI_API_KEY="sk-proj-..."
|
|
4175
|
+
`)),ao());let n=t;n||(n=(await(await So()).prompt([{type:"input",name:"query",message:"\u{1F916} What do you want to build?",validate:x=>x.length===0?"Please enter a description":x.length<3?"Please be more specific (at least 3 characters)":true}])).query),r.json||console.log(g.blue(`
|
|
940
4176
|
\u{1F916} Analyzing your request...
|
|
941
|
-
`)),await
|
|
4177
|
+
`)),await xo(!r.json)||(console.log(g.yellow(`
|
|
942
4178
|
\u26A0\uFE0F Cannot proceed without embeddings
|
|
943
|
-
`)),process.exit(1));let c=parseInt(
|
|
4179
|
+
`)),process.exit(1));let c=parseInt(r.number,10),a=await vo(n,c);if(a.length===0||a[0].score<.3)if(console.log(g.yellow(`
|
|
944
4180
|
\u26A0\uFE0F No matching modules found in RapidKit registry.
|
|
945
|
-
`)),console.log(
|
|
946
|
-
`)),console.log(
|
|
947
|
-
`)),console.log(
|
|
948
|
-
`)),console.log(
|
|
949
|
-
`)),a.length>0)console.log(
|
|
950
|
-
`));else return;if(
|
|
951
|
-
`)),a.forEach((
|
|
952
|
-
`));let d=await
|
|
953
|
-
\u{1F4E6} Installing ${
|
|
954
|
-
`)),console.log(
|
|
955
|
-
\u26A0\uFE0F Note: Module installation not yet implemented`)),console.log(
|
|
956
|
-
`))):console.log(
|
|
4181
|
+
`)),console.log(g.cyan(`\u{1F4A1} Options:
|
|
4182
|
+
`)),console.log(g.white("1. Create custom module:")),console.log(g.gray(" rapidkit modules scaffold <name> --category <category>")),console.log(g.gray(` Example: rapidkit modules scaffold blockchain-integration --category integrations
|
|
4183
|
+
`)),console.log(g.white("2. Search with different keywords")),console.log(g.gray(` Try more general terms (e.g., "storage" instead of "blockchain")
|
|
4184
|
+
`)),console.log(g.white("3. Request feature:")),console.log(g.gray(` https://github.com/getrapidkit/rapidkit/issues
|
|
4185
|
+
`)),a.length>0)console.log(g.yellow(`\u26A0\uFE0F Low confidence matches found:
|
|
4186
|
+
`));else return;if(r.json){console.log(JSON.stringify({query:n,recommendations:a},null,2));return}console.log(g.green.bold(`\u{1F4E6} Recommended Modules:
|
|
4187
|
+
`)),a.forEach((u,h)=>{let x=(u.score*100).toFixed(1),y=u.score>.8?" \u2B50":"";console.log(g.bold(`${h+1}. ${u.module.name}${y}`)),console.log(g.gray(` ${u.module.description}`)),console.log(g.cyan(` Match: ${x}%`)+g.gray(` - ${u.reason}`)),console.log(g.yellow(` Category: ${u.module.category}`)),u.module.dependencies.length>0&&console.log(g.magenta(` Requires: ${u.module.dependencies.join(", ")}`)),console.log();});let l=a.slice(0,3).map(u=>u.module.id);console.log(g.cyan("\u{1F4A1} Quick install (top 3):")),console.log(g.white(` rapidkit add module ${l.join(" ")}
|
|
4188
|
+
`));let d=await So(),{shouldInstall:p}=await d.prompt([{type:"confirm",name:"shouldInstall",message:"Would you like to install these modules now?",default:false}]);if(p){let{selectedModules:u}=await d.prompt([{type:"checkbox",name:"selectedModules",message:"Select modules to install:",choices:a.map(h=>({name:`${h.module.name} - ${h.module.description}`,value:h.module.id,checked:h.score>.7}))}]);u.length>0?(console.log(g.blue(`
|
|
4189
|
+
\u{1F4E6} Installing ${u.length} modules...
|
|
4190
|
+
`)),console.log(g.gray(`Command: rapidkit add module ${u.join(" ")}`)),console.log(g.yellow(`
|
|
4191
|
+
\u26A0\uFE0F Note: Module installation not yet implemented`)),console.log(g.gray(`Coming soon in next version!
|
|
4192
|
+
`))):console.log(g.gray(`
|
|
957
4193
|
No modules selected
|
|
958
4194
|
`));}}catch(i){a.error(`
|
|
959
|
-
\u274C Error:`,i.message),i.code==="invalid_api_key"?(console.log(
|
|
960
|
-
\u{1F4A1} Your API key may be invalid or expired`)),console.log(
|
|
961
|
-
`))):i.message.includes("embeddings file not found")&&(console.log(
|
|
962
|
-
\u{1F4A1} Module embeddings not generated yet`)),console.log(
|
|
963
|
-
`))),process.exit(1);}}),o.command("info").description("Show AI features information").action(()=>{let t=
|
|
4195
|
+
\u274C Error:`,i.message),i.code==="invalid_api_key"?(console.log(g.yellow(`
|
|
4196
|
+
\u{1F4A1} Your API key may be invalid or expired`)),console.log(g.cyan(` Update it: rapidkit config set-api-key
|
|
4197
|
+
`))):i.message.includes("embeddings file not found")&&(console.log(g.yellow(`
|
|
4198
|
+
\u{1F4A1} Module embeddings not generated yet`)),console.log(g.cyan(" Generate them (one-time):")),console.log(g.white(" cd rapidkit-npm")),console.log(g.white(' export OPENAI_API_KEY="sk-proj-..."')),console.log(g.white(` npx tsx src/ai/generate-embeddings.ts
|
|
4199
|
+
`))),process.exit(1);}}),o.command("info").description("Show AI features information").action(()=>{let t=xe(),r=wt();console.log(g.bold(`
|
|
964
4200
|
\u{1F916} RapidKit AI Features
|
|
965
|
-
`)),console.log(
|
|
4201
|
+
`)),console.log(g.cyan("Status:"),r?g.green("Enabled"):g.red("Disabled")),console.log(g.cyan("API Key:"),t?g.green("Configured \u2713"):g.red("Not configured \u2717")),console.log(g.bold(`
|
|
966
4202
|
\u{1F4E6} Available Features:
|
|
967
|
-
`)),console.log(
|
|
4203
|
+
`)),console.log(g.white("\u2022 Module Recommender")+g.gray(" - AI-powered module suggestions")),console.log(g.gray(' Usage: rapidkit ai recommend "I need authentication"')),console.log(g.bold(`
|
|
968
4204
|
\u{1F4B0} Pricing:
|
|
969
|
-
`)),console.log(
|
|
4205
|
+
`)),console.log(g.white("\u2022 Per query: ~$0.0002")+g.gray(" (practically free)")),console.log(g.white("\u2022 100 queries: ~$0.02")+g.gray(" (2 cents)")),console.log(g.white("\u2022 1000 queries: ~$0.20")+g.gray(" (20 cents)")),console.log(g.bold(`
|
|
970
4206
|
\u{1F680} Getting Started:
|
|
971
|
-
`)),t?(console.log(
|
|
4207
|
+
`)),t?(console.log(g.green("\u2713 You're all set!")),console.log(g.white(' Try: rapidkit ai recommend "user authentication"'))):(console.log(g.white("1. Get OpenAI API key: https://platform.openai.com/api-keys")),console.log(g.white("2. Configure: rapidkit config set-api-key")),console.log(g.white('3. Try: rapidkit ai recommend "user authentication"'))),console.log();}),o.command("generate-embeddings").description("Generate AI embeddings for all modules (one-time setup)").option("--force","Force regeneration even if embeddings exist").action(async()=>{try{let t=xe();t||(console.log(g.red(`
|
|
972
4208
|
\u274C OpenAI API key not configured
|
|
973
|
-
`)),console.log(
|
|
974
|
-
`)),console.log(
|
|
975
|
-
OR set environment variable:`)),console.log(
|
|
976
|
-
`)),process.exit(1)),
|
|
977
|
-
`))),process.exit(
|
|
4209
|
+
`)),console.log(g.cyan(`To generate embeddings, you need an OpenAI API key:
|
|
4210
|
+
`)),console.log(g.white("1. Get your key from: https://platform.openai.com/api-keys")),console.log(g.white("2. Configure it: rapidkit config set-api-key")),console.log(g.gray(`
|
|
4211
|
+
OR set environment variable:`)),console.log(g.white(` export OPENAI_API_KEY="sk-proj-..."
|
|
4212
|
+
`)),process.exit(1)),We(t);let r=await ze(true);r&&(console.log(g.green("\u2705 Ready to use AI recommendations!")),console.log(g.cyan(`Try: rapidkit ai recommend "authentication"
|
|
4213
|
+
`))),process.exit(r?0:1);}catch(t){a.error("Failed to generate embeddings:",t.message),process.exit(1);}}),o.command("update-embeddings").description("Update existing embeddings with latest modules").action(async()=>{try{let t=xe();t||(console.log(g.red(`
|
|
978
4214
|
\u274C OpenAI API key not configured
|
|
979
|
-
`)),console.log(
|
|
980
|
-
`)),process.exit(1)),
|
|
4215
|
+
`)),console.log(g.white(`Set your API key: rapidkit config set-api-key
|
|
4216
|
+
`)),process.exit(1)),We(t);let r=await Co();process.exit(r?0:1);}catch(t){a.error("Failed to update embeddings:",t.message),process.exit(1);}});}var Ye=class{constructor(o){this.runCommand=o;}runtime="go";async run(o,t,r){return {exitCode:await this.runCommand(o,t,r)}}async checkPrereqs(){return this.run("go",["version"],process.cwd())}async initProject(o){return this.run("go",["mod","tidy"],o)}async runDev(o){let t=v.join(o,"Makefile");return A.existsSync(t)?this.run("make",["run"],o):this.run("go",["run","./main.go"],o)}async runTest(o){return this.run("go",["test","./..."],o)}async runBuild(o){return this.run("go",["build","./..."],o)}async runStart(o){let t=v.join(o,"server");return A.existsSync(t)?this.run(t,[],o):this.run("go",["run","./main.go"],o)}async doctorHints(o){return ["Install Go from https://go.dev/dl/ if missing.","Run go mod tidy when dependencies are out of sync.","Use make run for hot-reload if Makefile exists."]}};var Qe=class{constructor(o){this.runCommand=o;}runtime="node";async run(o,t,r){return {exitCode:await this.runCommand(o,t,r)}}detectPackageManager(o){return A.existsSync(v.join(o,"pnpm-lock.yaml"))?"pnpm":A.existsSync(v.join(o,"yarn.lock"))?"yarn":"npm"}scriptArgs(o,t){return o==="npm"?["run",t]:["run",t]}async checkPrereqs(){return this.run("node",["--version"],process.cwd())}async initProject(o){let t=this.detectPackageManager(o);return this.run(t,["install"],o)}async runDev(o){let t=this.detectPackageManager(o);return this.run(t,this.scriptArgs(t,"dev"),o)}async runTest(o){let t=this.detectPackageManager(o);return this.run(t,this.scriptArgs(t,"test"),o)}async runBuild(o){let t=this.detectPackageManager(o);return this.run(t,this.scriptArgs(t,"build"),o)}async runStart(o){let t=this.detectPackageManager(o);return this.run(t,this.scriptArgs(t,"start"),o)}async doctorHints(o){return ["Install Node.js LTS and ensure node/npm are on PATH.","Use lockfiles (package-lock.json, pnpm-lock.yaml, yarn.lock) for deterministic installs.","Run install before dev/test/build if dependencies changed."]}};var Xe=class{constructor(o){this.runCore=o;}runtime="python";async run(o,t){return {exitCode:await this.runCore(o,t)}}async checkPrereqs(){return this.run(["doctor","--json"],process.cwd())}async initProject(o){return this.run(["init"],o)}async runDev(o){return this.run(["dev"],o)}async runTest(o){return this.run(["test"],o)}async runBuild(o){return this.run(["build"],o)}async runStart(o){return this.run(["start"],o)}async doctorHints(o){return ["Run rapidkit doctor --workspace for a full workspace scan.","Use rapidkit init after adding or changing modules.","Use workspace launcher ./rapidkit to avoid environment drift."]}};function Q(){return process.env.RAPIDKIT_ENABLE_RUNTIME_ADAPTERS==="1"}function fe(e,o){return e==="go"?new Ye((t,r,i)=>o.runCommandInCwd(t,r,i)):e==="node"?new Qe((t,r,i)=>o.runCommandInCwd(t,r,i)):new Xe((t,r)=>o.runCoreRapidkit(t,{cwd:r}))}var Ze=v.join(qo__default.homedir(),".rapidkit","cache"),Ao=1440*60*1e3,et=class e{static instance;memoryCache=new Map;constructor(){}static getInstance(){return e.instance||(e.instance=new e),e.instance}getCacheKey(o){return createHash("md5").update(o).digest("hex")}getCachePath(o){return v.join(Ze,`${this.getCacheKey(o)}.json`)}async get(o,t="1.0"){let r=this.memoryCache.get(o);if(r&&r.version===t&&Date.now()-r.timestamp<Ao)return a.debug(`Cache hit (memory): ${o}`),r.data;try{let i=this.getCachePath(o),n=await promises.readFile(i,"utf-8"),s=JSON.parse(n);if(s.version===t&&Date.now()-s.timestamp<Ao)return a.debug(`Cache hit (disk): ${o}`),this.memoryCache.set(o,s),s.data;await promises.unlink(i).catch(()=>{});}catch{a.debug(`Cache miss: ${o}`);}return null}async set(o,t,r="1.0"){let i={data:t,timestamp:Date.now(),version:r};this.memoryCache.set(o,i);try{await promises.mkdir(Ze,{recursive:true});let n=this.getCachePath(o);await promises.writeFile(n,JSON.stringify(i),"utf-8"),a.debug(`Cache set: ${o}`);}catch(n){a.debug(`Cache write failed: ${o}`,n);}}async invalidate(o){this.memoryCache.delete(o);try{let t=this.getCachePath(o);await promises.unlink(t),a.debug(`Cache invalidated: ${o}`);}catch{}}async clear(){this.memoryCache.clear();try{let o=await promises.readdir(Ze);await Promise.all(o.map(t=>promises.unlink(v.join(Ze,t)))),a.debug("Cache cleared");}catch{}}};function ie(e){let o=e;for(;;){let t=v.join(o,".rapidkit","project.json");if(A.existsSync(t))try{return JSON.parse(A.readFileSync(t,"utf8"))}catch{return null}let r=v.dirname(o);if(r===o)break;o=r;}return null}function _e(e,o){let t=e?.runtime?.toLowerCase(),r=e?.kit_name?.toLowerCase(),i=A.existsSync(v.join(o,"go.mod"));return t==="go"||(r?.startsWith("gofiber")??false)||(r?.startsWith("gogin")??false)||i}function he(e,o){let t=e?.runtime?.toLowerCase(),r=e?.kit_name?.toLowerCase(),i=A.existsSync(v.join(o,"package.json"));return t==="node"||t==="typescript"||(r?.startsWith("nestjs")??false)||i}function tt(e,o){let t=e?.runtime?.toLowerCase(),r=e?.kit_name?.toLowerCase(),i=A.existsSync(v.join(o,"pyproject.toml")),n=A.existsSync(v.join(o,"requirements.txt"))||A.existsSync(v.join(o,"requirements.in"));return t==="python"||(r?.startsWith("fastapi")??false)||i||n}function kt(e){if(!e||typeof e!="object")return null;let o=e.code;return o==="PYTHON_NOT_FOUND"||o==="BRIDGE_VENV_BOOTSTRAP_FAILED"?o:null}function ei(e){let o=e.trim().toLowerCase();return o?o.startsWith("fastapi")?"fastapi":o.startsWith("nestjs")?"nestjs":null:null}function St(e){let o=e.trim().toLowerCase();return o.startsWith("gofiber")||o==="go"||o==="go.standard"||o==="fiber"}function ot(e){let o=e.trim().toLowerCase();return o.startsWith("gogin")||o==="gin"}function it(e,o){let t=e.indexOf(o);if(t>=0&&t+1<e.length)return e[t+1];let r=e.find(i=>i.startsWith(`${o}=`));if(r)return r.slice(o.length+1)}async function No(e){if(e[0]!=="create"||e[1]!=="project")return 1;let o=e[2],t=e[3];if(!o||!t)return process.stderr.write(`Usage: rapidkit create project gofiber.standard <name> [--output <dir>]
|
|
4217
|
+
`),1;let r=it(e,"--output")||process.cwd(),i=v.resolve(r,t),n=e.includes("--skip-git")||e.includes("--no-git");try{let{default:s}=await import('fs-extra');if(await s.ensureDir(v.dirname(i)),await s.pathExists(i))return process.stderr.write(`\u274C Directory "${i}" already exists
|
|
4218
|
+
`),1;await s.ensureDir(i),await gt(i,{project_name:t,module_path:t,skipGit:n});let c=ce(process.cwd());if(c){let{syncWorkspaceProjects:a}=await import('./workspace-LZZGJRGV.js');await a(c,true);}return 0}catch(s){return process.stderr.write(`RapidKit Go/Fiber generator failed: ${s?.message??s}
|
|
4219
|
+
`),1}}async function jo(e){if(e[0]!=="create"||e[1]!=="project")return 1;let o=e[2],t=e[3];if(!o||!t)return process.stderr.write(`Usage: rapidkit create project gogin.standard <name> [--output <dir>]
|
|
4220
|
+
`),1;let r=it(e,"--output")||process.cwd(),i=v.resolve(r,t),n=e.includes("--skip-git")||e.includes("--no-git");try{let{default:s}=await import('fs-extra');if(await s.ensureDir(v.dirname(i)),await s.pathExists(i))return process.stderr.write(`\u274C Directory "${i}" already exists
|
|
4221
|
+
`),1;await s.ensureDir(i),await mt(i,{project_name:t,module_path:t,skipGit:n});let c=ce(process.cwd());if(c){let{syncWorkspaceProjects:a}=await import('./workspace-LZZGJRGV.js');await a(c,true);}return 0}catch(s){return process.stderr.write(`RapidKit Go/Gin generator failed: ${s?.message??s}
|
|
4222
|
+
`),1}}async function Rt(e,o){if(e.includes("--json"))return process.stderr.write("RapidKit (npm) offline fallback does not support --json for `create` commands.\nInstall Python 3.10+ and retry the same command.\n"),1;if(e[0]!=="create")return 1;if(e[1]!=="project")return process.stderr.write(`RapidKit (npm) could not run the Python core engine for \`create\`.
|
|
981
4223
|
Reason: ${o}.
|
|
982
4224
|
Install Python 3.10+ to use the interactive wizard and full kit catalog.
|
|
983
|
-
`),1;let i=e[2],
|
|
4225
|
+
`),1;let i=e[2],n=e[3];if(!i||!n)return process.stderr.write(`Usage: rapidkit create project <kit> <name> [--output <dir>]
|
|
984
4226
|
Tip: offline fallback supports only fastapi* and nestjs* kits.
|
|
985
|
-
`),1;let s=
|
|
4227
|
+
`),1;let s=ei(i);if(!s)return process.stderr.write(`RapidKit (npm) could not run the Python core engine to create this kit.
|
|
986
4228
|
Reason: ${o}.
|
|
987
4229
|
Requested kit: ${i}
|
|
988
4230
|
Offline fallback only supports: fastapi.standard, nestjs.standard (and their shorthands).
|
|
989
4231
|
Install Python 3.10+ to access all kits.
|
|
990
|
-
`),1;let c=
|
|
991
|
-
`),1;let
|
|
992
|
-
`),1}}async function
|
|
993
|
-
`),1;try{
|
|
994
|
-
`),1;throw
|
|
995
|
-
`),1;let l=await
|
|
996
|
-
`),1}try{if(e[0]==="create"&&e[1]==="project"){
|
|
997
|
-
|
|
998
|
-
`)
|
|
999
|
-
`),
|
|
1000
|
-
`),1)}}
|
|
1001
|
-
`),1)}return
|
|
4232
|
+
`),1;let c=it(e,"--output")||process.cwd(),a=v.resolve(c,n),l=e.includes("--skip-git")||e.includes("--no-git"),d=e.includes("--skip-install");try{if(await R.ensureDir(v.dirname(a)),await R.pathExists(a))return process.stderr.write(`\u274C Directory "${a}" already exists
|
|
4233
|
+
`),1;let p="pip",u=ce(process.cwd());if(u)try{let{readWorkspaceMarker:h}=await import('./workspace-marker-IOPQ42A7.js'),x=await h(u);x?.metadata?.npm?.installMethod&&(p=x.metadata.npm.installMethod,console.log(`[DEBUG] Detected workspace engine: ${p}`));}catch(h){console.log("[DEBUG] Failed to read workspace marker:",h);}else console.log("[DEBUG] No workspace found, using default engine: pip");if(await R.ensureDir(a),await Wt(a,{project_name:n,template:s,kit_name:i,skipGit:l,skipInstall:d,engine:p}),u){let{syncWorkspaceProjects:h}=await import('./workspace-LZZGJRGV.js');await h(u,true);}return 0}catch(p){return process.stderr.write(`RapidKit (npm) offline fallback failed: ${p?.message??p}
|
|
4234
|
+
`),1}}async function ti(e){let o=new Set(["--yes","-y","--skip-git","--skip-install","--debug","--dry-run","--no-update-check","--create-workspace","--no-workspace"]);if(e[0]==="create"&&e[1]==="workspace")try{let t=e.includes("--yes")||e.includes("-y"),r=e.includes("--skip-git")||e.includes("--no-git"),i=e[2]&&!e[2].startsWith("-")?e[2]:void 0,n=it(e,"--install-method"),s=n==="poetry"||n==="venv"||n==="pipx"?n:void 0,c=i||(t?"my-workspace":(await ae.prompt([{type:"input",name:"workspaceName",message:"Workspace name:",default:"my-workspace"}])).workspaceName);if(!c||!c.trim())return process.stderr.write(`Workspace name is required.
|
|
4235
|
+
`),1;try{ct(c);}catch(p){if(p instanceof H)return process.stderr.write(`${p.message}
|
|
4236
|
+
`),1;throw p}let a=v.resolve(process.cwd(),c);if(await R.pathExists(a))return process.stderr.write(`\u274C Directory "${c}" already exists
|
|
4237
|
+
`),1;let l=await Z(),d=l.author||process.env.USER||"RapidKit User";if(!t){let p=await ae.prompt([{type:"input",name:"author",message:"Author name:",default:d}]);p.author?.trim()&&(d=p.author.trim());}return await Ge(c,{skipGit:r,yes:t,userConfig:{...l,author:d},installMethod:s}),0}catch(t){return process.stderr.write(`RapidKit (npm) failed to create workspace: ${t?.message??t}
|
|
4238
|
+
`),1}try{if(e[0]==="create"&&e[1]==="project"){if(!e[2]||e[2].startsWith("-")){console.log(g.bold(`
|
|
4239
|
+
\u{1F680} RapidKit
|
|
4240
|
+
`));let{kitChoice:a}=await ae.prompt([{type:"rawlist",name:"kitChoice",message:"Select a kit to scaffold:",choices:[{name:"fastapi \u2014 FastAPI Standard Kit",value:"fastapi.standard"},{name:"fastapi \u2014 FastAPI DDD Kit",value:"fastapi.ddd"},{name:"nestjs \u2014 NestJS Standard Kit",value:"nestjs.standard"},{name:"go/fiber \u2014 Go Fiber Standard Kit",value:"gofiber.standard"},{name:"go/gin \u2014 Go Gin Standard Kit",value:"gogin.standard"}]}]);if(St(a)||ot(a)){let{projectName:l}=await ae.prompt([{type:"input",name:"projectName",message:"Project name:",validate:p=>p.trim().length>0||"Project name is required"}]),d=e.slice(2).filter(p=>p.startsWith("-"));return ot(a)?await jo(["create","project",a,l.trim(),...d]):await No(["create","project",a,l.trim(),...d])}e.splice(2,0,a);}if(St(e[2]||""))return await No(e);if(ot(e[2]||""))return await jo(e);let t=e.includes("--create-workspace"),r=e.includes("--no-workspace"),i=e.includes("--yes")||e.includes("-y"),n=e.includes("--skip-git")||e.includes("--no-git");if(!!!rt(process.cwd())){if(t)await te(process.cwd(),{skipGit:n,yes:i,userConfig:await Z()});else if(!r)if(i)await te(process.cwd(),{skipGit:n,yes:true,userConfig:await Z()});else {let{createWs:a}=await ae.prompt([{type:"confirm",name:"createWs",message:"This project will be created outside a RapidKit workspace. Create and register a workspace here?",default:true}]);a&&await te(process.cwd(),{skipGit:n,yes:false,userConfig:await Z()});}}let c=e.filter(a=>{let l=a.split("=")[0];return !o.has(a)&&!o.has(l)});try{await c$1();let a$1=await d(c,{cwd:process.cwd()});if(a$1===0){let l=ce(process.cwd());if(l){try{let p=e[3];if(p){let u=e.indexOf("--output"),h=u>=0?e[u+1]:".",x=v.resolve(process.cwd(),h,p),y=v.join(l,".python-version"),C=v.join(x,".python-version");if(A.existsSync(y)&&A.existsSync(x)){let E=A.readFileSync(y,"utf-8");A.writeFileSync(C,E.trim()+`
|
|
4241
|
+
`),a.debug(`Synced Python version ${E.trim()} from workspace to ${p}`);}}}catch(p){a.debug("Could not sync Python version from workspace:",p);}let{syncWorkspaceProjects:d}=await import('./workspace-LZZGJRGV.js');await d(l,true);}}return a$1}catch(a){let l=kt(a);return l?await Rt(c,l):(process.stderr.write(`RapidKit (npm) failed to run the Python core engine: ${a?.message??a}
|
|
4242
|
+
`),1)}}if(e[0]==="create"&&e[1]!=="project")try{await c$1();let t=await d(e,{cwd:process.cwd()});if(t===0){let r=ce(process.cwd());if(r){let{syncWorkspaceProjects:i}=await import('./workspace-LZZGJRGV.js');await i(r,true);}}return t}catch(t){let r=kt(t);return r?await Rt(e,r):(process.stderr.write(`RapidKit (npm) failed to run the Python core engine: ${t?.message??t}
|
|
4243
|
+
`),1)}return await c$1(),await d(e,{cwd:process.cwd()})}catch(t){let r=kt(t);return r?await Rt(e,r):(process.stderr.write(`RapidKit (npm) failed to run the Python core engine: ${t?.message??t}
|
|
4244
|
+
`),1)}}var Go=["init","dev","start","build","test","docs","lint","format","create","help","--help","-h"];function Do(e){let o=e;for(;;){let t=v.join(o,".rapidkit","context.json");if(A.existsSync(t))return t;let r=v.dirname(o);if(r===o)break;o=r;}return null}function rt(e){let o=e;for(;;){let t=v.join(o,".rapidkit-workspace");if(A.existsSync(t))return t;let r=v.dirname(o);if(r===o)break;o=r;}return null}function ce(e){let o=e;for(;;){let t=v.join(o,".rapidkit-workspace");if(A.existsSync(t))return o;let r=v.dirname(o);if(r===o)break;o=r;}return null}async function J(e,o,t){return await new Promise(r=>{let i=spawn(e,o,{stdio:"inherit",cwd:t,shell:process.platform==="win32"});i.on("close",n=>r(n??1)),i.on("error",()=>r(1));})}async function $o(e){let o="poetry";try{let{readWorkspaceMarker:t}=await import('./workspace-marker-IOPQ42A7.js'),i=(await t(e))?.metadata?.npm?.installMethod;(i==="poetry"||i==="venv"||i==="pipx"||i==="pip")&&(o=i);}catch{}if(o==="poetry")return await J("poetry",["install","--no-root"],e);if(o==="venv"){let t=v.join(e,".venv",process.platform==="win32"?"Scripts":"bin",process.platform==="win32"?"python.exe":"python");return await R.pathExists(t)?await J(t,["-m","pip","install","--upgrade","rapidkit-core"],e):(process.stderr.write(`Workspace virtualenv not found (.venv).
|
|
4245
|
+
`),1)}return 0}async function oi(e){let o=await A.promises.readdir(e,{withFileTypes:true}),t=[];for(let r of o){if(!r.isDirectory()||r.name.startsWith("."))continue;let i=v.join(e,r.name),n=v.join(i,".rapidkit","context.json"),s=v.join(i,".rapidkit","project.json");(await R.pathExists(n)||await R.pathExists(s))&&t.push(i);}return t}function ri(e){let o="my-workspace",t=1;for(;;){let r=t===1?o:`${o}-${t}`,i=v.join(e,r);if(!A.existsSync(i))return {name:r,targetPath:i};t+=1;}}async function xt(e){return Q()?(await fe("go",{runCommandInCwd:J,runCoreRapidkit:d}).initProject(e)).exitCode:(console.log(g.cyan(`
|
|
4246
|
+
\u{1F439} Go project detected: ${v.basename(e)}`)),console.log(g.gray(`Running go mod tidy\u2026
|
|
4247
|
+
`)),await J("go",["mod","tidy"],e))}async function ni(e){if(Q())return (await fe("go",{runCommandInCwd:J,runCoreRapidkit:d}).runDev(e)).exitCode;let o=v.join(e,"Makefile");return A.existsSync(o)?(console.log(g.cyan(`
|
|
4248
|
+
\u{1F439} Starting Go/Fiber dev server (make run)\u2026
|
|
4249
|
+
`)),await J("make",["run"],e)):(console.log(g.cyan(`
|
|
4250
|
+
\u{1F439} Starting Go/Fiber dev server (go run ./main.go)\u2026
|
|
4251
|
+
`)),await J("go",["run","./main.go"],e))}async function Pe(e,o){if(!Q())return 1;let t=fe("node",{runCommandInCwd:J,runCoreRapidkit:d});return e==="init"?(await t.initProject(o)).exitCode:e==="dev"?(await t.runDev(o)).exitCode:e==="test"?(await t.runTest(o)).exitCode:e==="build"?(await t.runBuild(o)).exitCode:(await t.runStart(o)).exitCode}async function ii(e,o=Mo){let t=["init",...e.slice(1)];return await o(t)}async function si(e){let o=(e[1]||"").toLowerCase();if(!o||!["python","node","go"].includes(o))return console.log(g.yellow("Usage: rapidkit setup <python|node|go>")),1;if(!Q())return console.log(g.yellow("Runtime adapters are disabled. Set RAPIDKIT_ENABLE_RUNTIME_ADAPTERS=1 to use setup.")),1;let t=fe(o,{runCommandInCwd:J,runCoreRapidkit:d}),r=await t.checkPrereqs(),i=await t.doctorHints(process.cwd());if(r.exitCode===0?console.log(g.green(`\u2705 ${o} prerequisites look good.`)):console.log(g.red(`\u274C ${o} prerequisites check failed.`)),i.length>0){console.log(g.gray(`
|
|
4252
|
+
Hints:`));for(let n of i)console.log(g.gray(`- ${n}`));}return r.exitCode}async function ai(e){let o=(e[1]||"status").toLowerCase(),t=et.getInstance();return o==="clear"||o==="prune"||o==="repair"?(await t.clear(),console.log(g.green(`\u2705 Cache ${o} completed.`)),0):o==="status"?(console.log(g.cyan("RapidKit cache is enabled (memory + disk).")),console.log(g.gray("Use: rapidkit cache clear|prune|repair")),0):(console.log(g.yellow("Usage: rapidkit cache <status|clear|prune|repair>")),1)}async function Mo(e){let o=process.cwd(),t=Q(),r=t?fe("python",{runCommandInCwd:J,runCoreRapidkit:d}):null;if(e.length>1){let a=v.resolve(o,e[1]),l=ie(a);return _e(l,a)?await xt(a):t&&he(l,a)?await Pe("init",a):r&&tt(l,a)?(await r.initProject(a)).exitCode:await d(e,{cwd:o})}let i=ie(o);if(_e(i,o))return await xt(o);if(t&&he(i,o))return await Pe("init",o);if(r&&tt(i,o))return (await r.initProject(o)).exitCode;let n=ce(o),s=Do(o),c=s?v.dirname(v.dirname(s)):null;if(c&&c!==n)return r?(await r.initProject(c)).exitCode:await d(["init"],{cwd:c});if(n&&o===n){let a=await $o(n);if(a!==0)return a;let l=await oi(n);for(let d$1 of l){let p=ie(d$1);if(_e(p,d$1)){let u=await xt(d$1);if(u!==0)return u}else {if(t&&he(p,d$1)){let h=await Pe("init",d$1);if(h!==0)return h;continue}if(r&&tt(p,d$1)){let h=await r.initProject(d$1);if(h.exitCode!==0)return h.exitCode;continue}let u=await d(["init"],{cwd:d$1});if(u!==0)return u}}return 0}if(!n){let a=await Z(),{name:l}=ri(o);await Ge(l,{yes:true,userConfig:a});let d=v.join(o,l);return await $o(d)}return await d(e,{cwd:o})}async function ci(){let e=process.cwd(),o=process.argv.slice(2),t=A.existsSync(v.join(e,".rapidkit-workspace")),r=A.existsSync(v.join(e,".rapidkit","project.json"));if(o[0]==="create"||o[0]==="init"&&t&&!r)return false;try{let d$1=o[0],p=!d$1||d$1==="--help"||d$1==="-h"||d$1==="help";if(rt(e)&&p){let h=await d(d$1?["--help"]:[],{cwd:e});process.exit(h);}}catch{}try{let d$1=o[0],p=d$1==="shell"&&o[1]==="activate",u=d$1==="create",h=await Gt(e,{cwd:e,timeoutMs:1200});if(h.ok&&h.data?.isRapidkitProject&&h.data.engine==="python"&&!p&&!u){let x=await d(process.argv.slice(2),{cwd:e});process.exit(x);}}catch{}let i=Do(e),n=process.platform==="win32",s=n?[v.join(e,"rapidkit.cmd"),v.join(e,"rapidkit"),v.join(e,".rapidkit","rapidkit.cmd"),v.join(e,".rapidkit","rapidkit")]:[v.join(e,"rapidkit"),v.join(e,".rapidkit","rapidkit")],c=null;for(let d of s)if(await R.pathExists(d)){c=d;break}let a$1=o[0],l=a$1==="create";if(a$1==="init"&&t&&!r)return false;if(c&&a$1&&Go.includes(a$1)&&!l){a.debug(`Delegating to local CLI: ${c} ${o.join(" ")}`);let d=spawn(c,o,{stdio:"inherit",cwd:e,shell:n});return d.on("close",p=>{process.exit(p??0);}),d.on("error",p=>{a.error(`Failed to run local rapidkit: ${p.message}`),process.exit(1);}),true}if(i&&await R.pathExists(i))try{if((await R.readJson(i)).engine==="pip"){let p=o[0],h=process.platform==="win32"?[v.join(e,"rapidkit.cmd"),v.join(e,"rapidkit"),v.join(e,".rapidkit","rapidkit.cmd"),v.join(e,".rapidkit","rapidkit")]:[v.join(e,"rapidkit"),v.join(e,".rapidkit","rapidkit")],x=null;for(let C of h)if(await R.pathExists(C)){x=C;break}if(x&&p&&Go.includes(p)){a.debug(`Delegating to local CLI (early detection): ${x} ${o.join(" ")}`);let C=spawn(x,o,{stdio:"inherit",cwd:e});return C.on("close",E=>process.exit(E??0)),C.on("error",E=>{a.error(`Failed to run local rapidkit: ${E.message}`),process.exit(1);}),true}if(p==="shell"&&o[1]==="activate"){let C=`# RapidKit: activation snippet - eval "$(rapidkit shell activate)"
|
|
1002
4253
|
VENV='.venv'
|
|
1003
4254
|
if [ -f "$VENV/bin/activate" ]; then
|
|
1004
4255
|
. "$VENV/bin/activate"
|
|
@@ -1007,45 +4258,45 @@ elif [ -f "$VENV/bin/activate.fish" ]; then
|
|
|
1007
4258
|
fi
|
|
1008
4259
|
export RAPIDKIT_PROJECT_ROOT="$(pwd)"
|
|
1009
4260
|
export PATH="$(pwd)/.rapidkit:$(pwd):$PATH"
|
|
1010
|
-
`;console.log(
|
|
4261
|
+
`;console.log(g.green.bold(`
|
|
1011
4262
|
\u2705 Activation snippet \u2014 run the following to activate this project in your current shell:
|
|
1012
|
-
`)),console.log(
|
|
4263
|
+
`)),console.log(C),console.log(g.gray(`
|
|
1013
4264
|
\u{1F4A1} After activation you can run: rapidkit dev
|
|
1014
|
-
`)),process.exit(0);}let y=await d(o,{cwd:e});process.exit(y);}}catch{}return false}var
|
|
4265
|
+
`)),process.exit(0);}let y=await d(o,{cwd:e});process.exit(y);}}catch{}return false}var X=null,nt=false,z=new Command;async function li(e){if(e.length===0)return false;let o=e[0],t=e[1];if(o==="shell"&&t==="activate"||o==="workspace"||o==="doctor"||o==="bootstrap"||o==="setup"||o==="cache"||o==="ai"||o==="config")return false;if(e.includes("--tui"))return true;if(o==="--help"||o==="-h"||o==="help"||o==="--version"||o==="-V"||e.includes("--template")||e.includes("-t"))return false;let r=new Set(["--yes","-y","--skip-git","--skip-install","--debug","--dry-run","--no-update-check","--create-workspace","--no-workspace"]);if(e.some(n=>r.has(n)))return false;let i=await h();return i?i.has(o):!!(a$1.has(o)||e.length>1)}z.name("rapidkit").description("Create RapidKit workspaces and projects").version(c());z.addHelpText("beforeAll",`RapidKit
|
|
1015
4266
|
|
|
1016
4267
|
Global CLI
|
|
1017
4268
|
Create RapidKit workspaces and projects
|
|
1018
4269
|
|
|
1019
4270
|
Global Engine Commands
|
|
1020
4271
|
Access engine-level commands when inside a RapidKit workspace or via the core bridge
|
|
1021
|
-
`);
|
|
4272
|
+
`);z.addHelpText("afterAll",`
|
|
1022
4273
|
Project Commands
|
|
1023
4274
|
rapidkit create
|
|
1024
4275
|
rapidkit init
|
|
1025
4276
|
rapidkit dev
|
|
1026
4277
|
|
|
1027
4278
|
Use "rapidkit help <command>" for more information.
|
|
1028
|
-
`);
|
|
4279
|
+
`);z.argument("[name]","Name of the workspace or project directory").addOption(new Option("-t, --template <template>","Legacy: create a project with template (fastapi, nestjs) instead of a workspace").hideHelp()).option("-y, --yes","Skip prompts and use defaults").addOption(new Option("--skip-git","Skip git initialization").hideHelp()).addOption(new Option("--skip-install","Legacy: skip installing dependencies (template mode)").hideHelp()).option("--debug","Enable debug logging").addOption(new Option("--dry-run","Show what would be created without creating it").hideHelp()).addOption(new Option("--install-method <method>","Installation method: poetry, venv, or pipx").choices(["poetry","venv","pipx"]).hideHelp()).addOption(new Option("--create-workspace","When creating a project outside a workspace: create and register a workspace in the current directory").hideHelp()).addOption(new Option("--no-workspace","When creating a project outside a workspace: do not create a workspace").hideHelp()).option("--no-update-check","Skip checking for updates").action(async(e$1,o)=>{try{o.debug&&(a.setDebug(true),a.debug("Debug mode enabled"));let t=await Z();a.debug("User config loaded",t);let r=await Nt();a.debug("RapidKit config loaded",r);let i=jt(t,r,{author:o.author,skipGit:o.skipGit});a.debug("Merged config",i),o.updateCheck!==false&&await b(),console.log(g.blue.bold(`
|
|
1029
4280
|
\u{1F680} Welcome to RapidKit!
|
|
1030
|
-
`)),e||(
|
|
4281
|
+
`)),e$1||(pi(),process.exit(0));try{ct(e$1);}catch(c){throw c instanceof H&&(a.error(`
|
|
1031
4282
|
\u274C ${c.message}`),c.details&&a.warn(`\u{1F4A1} ${c.details}
|
|
1032
|
-
`),process.exit(1)),c}let
|
|
1033
|
-
\u274C Directory "${e}" already exists`),console.log(
|
|
4283
|
+
`),process.exit(1)),c}let n=v.resolve(process.cwd(),e$1);X=n,await R.pathExists(n)&&(a.error(`
|
|
4284
|
+
\u274C Directory "${e$1}" already exists`),console.log(g.cyan(`
|
|
1034
4285
|
\u{1F4A1} Choose a different name or delete the existing directory.
|
|
1035
|
-
`)),process.exit(1));let s=!!o.template;if(o.dryRun){console.log(
|
|
4286
|
+
`)),process.exit(1));let s=!!o.template;if(o.dryRun){console.log(g.cyan(`
|
|
1036
4287
|
\u{1F50D} Dry-run mode - showing what would be created:
|
|
1037
|
-
`)),console.log(
|
|
1038
|
-
`)),s){let c=String(o.template||"").trim(),a$1=c.toLowerCase(),l=a$1==="fastapi"?"fastapi.standard":a$1==="nestjs"?"nestjs.standard":c;if(!!!
|
|
1039
|
-
`),a.debug(`Synced Python version ${E.trim()} from workspace to project`);}}catch(E){a.debug("Could not sync Python version from workspace:",E);}}if(!o.skipInstall){let
|
|
1040
|
-
`),a.debug(`Re-synced Python version ${
|
|
4288
|
+
`)),console.log(g.white("\u{1F4C2} Path:"),n),console.log(g.white("\u{1F4E6} Type:"),s?`Project (${o.template})`:"Workspace"),console.log();return}if(!o.yes&&!s?await ae.prompt([{type:"input",name:"author",message:"Author name:",default:process.env.USER||"RapidKit User"}]):o.yes&&console.log(g.gray(`Using default values (--yes flag)
|
|
4289
|
+
`)),s){let c=String(o.template||"").trim(),a$1=c.toLowerCase(),l=a$1==="fastapi"?"fastapi.standard":a$1==="nestjs"?"nestjs.standard":a$1==="go"||a$1==="fiber"?"gofiber.standard":a$1==="gin"?"gogin.standard":c;if(St(l)){let x=v.resolve(process.cwd(),e$1);await gt(x,{project_name:e$1,module_path:e$1,skipGit:o.skipGit});return}if(ot(l)){let x=v.resolve(process.cwd(),e$1);await mt(x,{project_name:e$1,module_path:e$1,skipGit:o.skipGit});return}if(!!!rt(process.cwd())){if(o.createWorkspace)await te(process.cwd(),{skipGit:o.skipGit,yes:o.yes,userConfig:t});else if(!o.noWorkspace)if(o.yes)await te(process.cwd(),{skipGit:o.skipGit,yes:true,userConfig:t});else {let{createWs:x}=await ae.prompt([{type:"confirm",name:"createWs",message:"This project will be created outside a RapidKit workspace. Create and register a workspace here?",default:true}]);x&&await te(process.cwd(),{skipGit:o.skipGit,yes:false,userConfig:t});}}let p=["create","project",l,e$1,"--output",process.cwd(),"--install-essentials"],u=await e(p,{cwd:process.cwd()});u!==0&&process.exit(u);let h=rt(process.cwd());if(h){let x=v.dirname(h),y=v.join(x,".python-version"),C=v.join(n,".python-version");try{if(await R.pathExists(y)){let E=A.readFileSync(y,"utf-8");A.writeFileSync(C,E.trim()+`
|
|
4290
|
+
`),a.debug(`Synced Python version ${E.trim()} from workspace to project`);}}catch(E){a.debug("Could not sync Python version from workspace:",E);}}if(!o.skipInstall){let x=await d(["init",n],{cwd:process.cwd()});if(x!==0&&process.exit(x),h){let y=v.dirname(h),C=v.join(y,".python-version"),E=v.join(n,".python-version");try{if(await R.pathExists(C)){let q=A.readFileSync(C,"utf-8");A.writeFileSync(E,q.trim()+`
|
|
4291
|
+
`),a.debug(`Re-synced Python version ${q.trim()} after init`);}}catch(q){a.debug("Could not re-sync Python version after init:",q);}}}}else await Ge(e$1,{skipGit:o.skipGit,dryRun:o.dryRun,yes:o.yes,userConfig:i,installMethod:o.installMethod});}catch(t){t instanceof H?(a.error(`
|
|
1041
4292
|
\u274C ${t.message}`),t.details&&a.warn(`\u{1F4A1} ${t.details}`),a.debug("Error code:",t.code)):(a.error(`
|
|
1042
|
-
\u274C An unexpected error occurred:`),console.error(t)),process.exit(1);}finally{
|
|
1043
|
-
`)),console.log(
|
|
1044
|
-
`)),console.log(
|
|
1045
|
-
`)),console.log(
|
|
1046
|
-
`)),console.log(
|
|
1047
|
-
`));}process.on("SIGINT",async()=>{if(!
|
|
1048
|
-
|
|
1049
|
-
\u26A0\uFE0F Interrupted by user`)),
|
|
1050
|
-
`),o[0]==="create"){let
|
|
1051
|
-
`),t){let
|
|
4293
|
+
\u274C An unexpected error occurred:`),console.error(t)),process.exit(1);}finally{X=null;}});_o(z);so(z);z.command("shell <action>").description("Shell helpers (activate virtualenv in current shell)").action(async e=>{e!=="activate"&&(console.log(g.red(`Unknown shell command: ${e}`)),process.exit(1));let o=process.cwd();function t(a){let l=a;for(;;){let d=v.join(l,".rapidkit","context.json");if(A.existsSync(d))return d;let p=v.dirname(l);if(p===l)break;l=p;}return null}let r=t(o);function i(a){let l=a;for(;;){let d=v.join(l,".venv"),p=v.join(l,".rapidkit","activate");if(A.existsSync(p)||A.existsSync(d))return {venv:d,activateFile:p};let u=v.dirname(l);if(u===l)break;l=u;}return null}let n=i(o);!r&&!n&&(console.log(g.yellow("No RapidKit project found in this directory")),process.exit(1));let s;n&&A.existsSync(n.activateFile)?s=n.activateFile:n&&A.existsSync(n.venv)?s=process.platform==="win32"?v.join(n.venv,"Scripts","activate"):v.join(n.venv,"bin","activate"):(console.log(g.yellow("No virtual environment found")),process.exit(1));let c=process.platform==="win32";console.log(c?`call "${s}"`:`. "${s}"`);});z.command("doctor").description("\u{1FA7A} Check RapidKit environment health").option("--workspace","Check entire workspace (including all projects)").option("--json","Output results in JSON format (for CI/CD pipelines)").option("--fix","Automatically fix common issues (with confirmation)").action(async e=>{await ro(e);});z.command("workspace <action>").description("Manage RapidKit workspaces (list, sync)").action(async e=>{if(e==="list"){let{listWorkspaces:o}=await import('./workspace-LZZGJRGV.js');await o();}else if(e==="sync"){let o=ce(process.cwd());o||(console.log(g.red("\u274C Not inside a RapidKit workspace")),console.log(g.gray("\u{1F4A1} Run this command from within a workspace directory")),process.exit(1));let{syncWorkspaceProjects:t}=await import('./workspace-LZZGJRGV.js');console.log(g.cyan(`\u{1F4C2} Scanning workspace: ${v.basename(o)}`)),await t(o);}else console.log(g.red(`Unknown workspace action: ${e}`)),console.log(g.gray("Available: list, sync")),process.exit(1);});function pi(){console.log(g.white(`Usage:
|
|
4294
|
+
`)),console.log(g.cyan(" npx rapidkit <workspace-name> [options]")),console.log(g.cyan(` npx rapidkit create <...>
|
|
4295
|
+
`)),console.log(g.bold("Recommended workflow:")),console.log(g.cyan(" npx rapidkit my-workspace")),console.log(g.cyan(" cd my-workspace")),console.log(g.cyan(" npx rapidkit create project fastapi.standard my-api --output .")),console.log(g.cyan(" cd my-api")),console.log(g.cyan(` npx rapidkit init && npx rapidkit dev
|
|
4296
|
+
`)),console.log(g.bold("Options (workspace creation):")),console.log(g.gray(" -y, --yes Skip prompts and use defaults")),console.log(g.gray(" --skip-git Skip git initialization")),console.log(g.gray(" --debug Enable debug logging")),console.log(g.gray(" --dry-run Show what would be created")),console.log(g.gray(" --create-workspace When creating a project outside a workspace: create and register a workspace in the current directory")),console.log(g.gray(" --no-workspace When creating a project outside a workspace: do not create a workspace")),console.log(g.gray(` --no-update-check Skip checking for updates
|
|
4297
|
+
`)),console.log(g.gray(`Tip: set RAPIDKIT_SHOW_LEGACY=1 to show legacy template flags in help.
|
|
4298
|
+
`));}process.on("SIGINT",async()=>{if(!nt){if(nt=true,console.log(g.yellow(`
|
|
4299
|
+
|
|
4300
|
+
\u26A0\uFE0F Interrupted by user`)),X&&await R.pathExists(X)){console.log(g.gray("Cleaning up partial installation..."));try{await R.remove(X),console.log(g.green("\u2713 Cleanup complete"));}catch(e){a.debug("Cleanup failed:",e);}}process.exit(130);}});process.on("SIGTERM",async()=>{if(!nt){if(nt=true,a.debug("Received SIGTERM"),X&&await R.pathExists(X))try{await R.remove(X);}catch(e){a.debug("Cleanup failed:",e);}process.exit(143);}});var ui=process.env.VITEST==="true"||process.env.VITEST==="1"||process.env.NODE_ENV==="test",gi=(()=>{let e=process.argv[1];if(!e)return false;try{return A.realpathSync(e)===A.realpathSync(fileURLToPath(import.meta.url))}catch{return v.resolve(e)===v.resolve(fileURLToPath(import.meta.url))}})(),mi=!ui||gi;mi&&ci().then(async e=>{if(!e){let o=process.argv.slice(2);if(process.env.RAPIDKIT_NPM_DEBUG_ARGS==="1"&&process.stderr.write(`[rapidkit-npm] argv=${JSON.stringify(o)}
|
|
4301
|
+
`),o[0]==="create"){let r=await ti(o);process.exit(r);}if(o[0]==="init"){let r=await Mo(o);process.exit(r);}if(o[0]==="bootstrap"){let r=await ii(o);process.exit(r);}if(o[0]==="setup"){let r=await si(o);process.exit(r);}if(o[0]==="cache"){let r=await ai(o);process.exit(r);}if(o[0]==="dev"){let r=ie(process.cwd());if(_e(r,process.cwd())){let i=await ni(process.cwd());process.exit(i);}if(Q()&&he(r,process.cwd())){let i=await Pe("dev",process.cwd());process.exit(i);}}if((o[0]==="test"||o[0]==="build"||o[0]==="start")&&Q()){let r=ie(process.cwd());if(he(r,process.cwd())){let i=await Pe(o[0],process.cwd());process.exit(i);}}if(o[0]==="add"||o[0]==="module"&&o[1]==="add"){let r=ie(process.cwd());(r?.runtime==="go"||r?.module_support===false)&&(console.error(g.red("\u274C RapidKit modules are not available for Go projects.")),console.error(g.gray(" The module system requires Python and is only supported for FastAPI and NestJS projects.")),process.exit(1));}let t=await li(o);if(process.env.RAPIDKIT_NPM_DEBUG_ARGS==="1"&&process.stderr.write(`[rapidkit-npm] shouldForwardToCore=${t}
|
|
4302
|
+
`),t){let r=await d(o,{cwd:process.cwd()});process.exit(r);}z.parse();}});export{ii as handleBootstrapCommand,ai as handleCacheCommand,ti as handleCreateOrFallback,Mo as handleInitCommand,si as handleSetupCommand,li as shouldForwardToCore};
|