UNPKG

create-cen-app

Version:
40 lines (34 loc) 18.3 kB
#!/usr/bin/env node import*as m from"@clack/prompts";import E from"chalk";import{execSync as D}from"child_process";import{execa as P}from"execa";import M from"fs-extra";import T from"path";var pe=e=>{try{return D("git --version",{cwd:e}),!0}catch{return!1}},me=e=>M.existsSync(T.join(e,".git")),ge=async e=>{try{return await P("git",["rev-parse","--is-inside-work-tree"],{cwd:e,stdout:"ignore"}),!0}catch{return!1}},de=()=>{let t=D("git --version").toString().trim().split(" ")[2],i=t?.split(".")[0],o=t?.split(".")[1];return{major:Number(i),minor:Number(o)}},ue=()=>D("git config --global init.defaultBranch || echo main").toString().trim(),W=async e=>{if(m.log.info("Initializing Git..."),!pe(e)){m.log.warn("Git is not installed. Skipping Git initialization.");return}m.log.info("Creating a new git repo...");let t=me(e),i=await ge(e),o=T.parse(e).name;if(i&&t){let n=await m.confirm({message:`${E.redBright.bold("Warning:")} Git is already initialized in "${o}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`,initialValue:!1});if(m.isCancel(n)){m.cancel("Operation cancelled. Skipping Git initialization.");return}if(!n){m.log.info("Skipping Git initialization.");return}M.removeSync(T.join(e,".git"))}else if(i&&!t){let n=await m.confirm({message:`${E.redBright.bold("Warning:")} "${o}" is already in a git worktree. Would you still like to initialize a new git repository in this directory?`,initialValue:!1});if(m.isCancel(n)){m.cancel("Operation cancelled. Skipping Git initialization.");return}if(!n){m.log.info("Skipping Git initialization.");return}}try{let n=ue(),{major:a,minor:s}=de();a<2||a==2&&s<28?(await P("git",["init"],{cwd:e}),await P("git",["symbolic-ref","HEAD",`refs/heads/${n}`],{cwd:e})):await P("git",["init",`--initial-branch=${n}`],{cwd:e}),await P("git",["add","."],{cwd:e}),m.log.success(`Successfully initialized and staged ${E.green.bold("git")} `)}catch{m.log.error(`Failed: could not initialize git. Update git to the latest version! `)}};import we from"fs-extra";import be from"path";import U from"path";import{fileURLToPath as fe}from"url";var he=fe(import.meta.url),ye=U.dirname(he),A=U.join(ye,"../"),z=` ___ ___ ___ __ _____ ___ ___ ___ _ _ __ ___ __ / __| _ \\ __| / \\_ _| __| / __|___| \\ | | / \\ | _ \\ _ \\ | (__| / _| / /\\ \\| | | _| | (__| _|| \\ | / /\\ \\| _/ _/ \\___|_|_\\___|_/\u203E\u203E\\_\\_| |___| \\___|___|_| \\_| /_/\u203E\u203E\\_\\_| |_|`,N="my-cen-app",O="create-cen-app",V="git@github.ibm.com:technology-garage-dach/full-stack-cen-template.git";var k=()=>{let e=be.join(A,"package.json");return we.readJSONSync(e).version??"1.0.0"};import*as y from"@clack/prompts";import{execSync as ve}from"child_process";import ke from"https";var B=e=>{let t=k();t.includes("beta")?(y.log.warn(" You are using a beta version of create-cen-app."),y.log.warn(" Please report any bugs you encounter.")):t.includes("next")?(y.log.warn(" You are running create-cen-app with the @next tag which is no longer maintained."),y.log.warn(" Please run the CLI with @latest instead.")):t!==e&&(y.log.warn(" You are using an outdated version of create-cen-app."),y.log.warn(` Your version: ${t}. Latest version in the npm registry: ${e}`),y.log.warn(" Please run the CLI with @latest to get the latest updates.")),console.log("")};function _e(){return new Promise((e,t)=>{ke.get("https://registry.npmjs.org/-/package/create-cen-app/dist-tags",i=>{if(i.statusCode===200){let o="";i.on("data",n=>o+=n),i.on("end",()=>{e(JSON.parse(o).latest)})}else t()}).on("error",()=>{t()})})}var J=()=>_e().catch(()=>{try{return ve("npm view create-cen-app version").toString().trim()}catch{return null}});import*as S from"@clack/prompts";import le from"fs-extra";import ce from"path";import*as p from"@clack/prompts";import{isCancel as $}from"@clack/prompts";import _ from"chalk";import{Command as xe}from"commander";var Y=[{value:"main",name:"main",description:"Carbon UI + built-in auth"},{value:"oauth-proxy",name:"oauth-proxy",description:"Carbon UI + auth via oauth proxy & AppID"},{value:"backend-only",name:"backend-only",description:"API-only with API key auth"},{value:"backend-only-no-db",name:"backend-only-no-db",description:"API-only with API key auth, no database"},{value:"main-custom-ui",name:"main-custom-ui",description:"shadcn UI + built-in auth"},{value:"oauth-proxy-custom-ui",name:"oauth-proxy-custom-ui",description:"shadcn UI + auth via oauth proxy & AppID"}];var w=()=>{let e=process.env.npm_config_user_agent;return e?e.startsWith("yarn")?"yarn":e.startsWith("pnpm")?"pnpm":"npm":"npm"};var H=e=>(e.length>1&&e.endsWith("/")&&(e=e.slice(0,-1)),e);var Pe=/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/,K=e=>{let t=H(e),i=t.split("/"),o=i.findIndex(a=>a.startsWith("@")),n=i[i.length-1];if(i.findIndex(a=>a.startsWith("@"))!==-1&&(n=i.slice(o).join("/")),!(t==="."||Pe.test(n??"")))return"App name must consist of only lowercase alphanumeric characters, '-', and '_'"};var R={appName:N,flavour:"main",flags:{noGit:!1,noInstall:!1,default:!1}},X=async()=>{let e=R,t=new xe().name(O);t.description("A CLI for creating web applications with the t3 stack").argument("[dir]","The name of the application, as well as the name of the directory to create").option("--noGit","Explicitly tell the CLI to not initialize a new git repo in the project",!1).option("--noInstall","Explicitly tell the CLI to not run the package manager's install command",!1).option("-y, --default","Bypass the CLI and use all default options to bootstrap a new cen-app",!1).version(k(),"-v, --version","Display the version number").addHelpText("afterAll",` The t3 stack was inspired by ${_.hex("#E8DCFF").bold("@t3dotgg")} and has been used to build awesome fullstack applications like ${_.hex("#E24A8D").underline("https://ping.gg")} `).parse(process.argv),process.env.npm_config_user_agent?.startsWith("yarn/3")&&p.log.warn("WARNING: Yarn 3 is currently not supported and may cause crashes. Please use pnpm, npm, or Yarn Classic instead.");let i=t.args[0];i&&(e.appName=i),e.flags=t.opts();try{if(process.env.TERM_PROGRAM?.toLowerCase().includes("mintty"))throw p.log.warn("WARNING: MinTTY detected which is non-interactive. Please use another terminal like Windows Terminal, or provide CLI arguments directly."),Object.assign(new Error("Non-interactive environment"),{isTTYError:!0});if(!e.flags.default){if(!i){let n=await p.text({message:"What will your project be called?",defaultValue:R.appName,placeholder:R.appName,validate:K});$(n)&&(p.cancel("Operation cancelled"),process.exit(0)),n&&(e.appName=n)}let o=await p.select({message:"Which flavour would you like to use?",options:Y.map(n=>({label:n.name,value:n.value,hint:n.description})),initialValue:"default"});if($(o)&&(p.cancel("Operation cancelled"),process.exit(0)),e.flavour=o,!e.flags.noInstall){let n=w(),a=n==="yarn"?n:`${n} install`,s=await p.confirm({message:o==="backend-only"||o==="backend-only-no-db"?`Would you like us to run ${_.magenta("'uv sync'")}?`:o==="go"||o==="java"?`Would you like us to run ${_.magenta(`'${a}'`)}?`:`Would you like us to run ${_.magenta(`'${a}'`)} and ${_.magenta("'uv sync'")}?`,initialValue:!0});$(s)&&(p.cancel("Operation cancelled"),process.exit(0)),e.flags.noInstall=!s}e.flags.noGit=!0}}catch(o){if(o instanceof Error&&"isTTYError"in o){p.log.warn(` ${O} needs an interactive terminal to provide options`);let n=await p.confirm({message:"Continue scaffolding a default CEN app?",initialValue:!0});$(n)&&(p.cancel("Operation cancelled"),process.exit(0)),n||(p.log.info("Exiting..."),process.exit(0)),p.log.info(`Bootstrapping a default CEN app in ./${e.appName}`)}else throw o}return e};import*as r from"@clack/prompts";import c from"chalk";import{execa as x}from"execa";import j from"fs-extra";import{execa as F}from"execa";var q=async()=>{try{let e=await Ie(),t=[];for(let o of e){let n=await Ce(o);t.push(...n)}if(t.length===0)return null;let i=[];for(let o of t){let n=await Ae(o);n&&(o.startsWith("/usr/bin")?i.push({path:o,version:n,owner:"system"}):i.push({path:o,version:n,owner:"user"}))}return i=i.filter((o,n,a)=>n===a.findIndex(s=>s.path===o.path)),i=i.sort((o,n)=>o.version&&n.version?Ne(o.version,n.version):0),i}catch{return null}};async function Ie(){try{let{stdout:e}=await F("compgen",["-c","|","grep","-E","'^python[0-9.]*$'"],{shell:"bash"});return e.trim().split(` `)}catch{return[]}}async function Ce(e){try{let{stdout:t}=await F("type",["-a",e],{shell:"bash"}),i=t.trim(),o=new RegExp(`(?<=${e} is ).+`,"gm");return i.match(o)||[]}catch{return[]}}async function Ae(e){try{let{stdout:t}=await F(e,["--version"]);return t.trim()}catch{return null}}function Ne(e,t){let i=e.split(".").map(Number),o=t.split(".").map(Number);for(let n=0;n<Math.max(i.length,o.length);n++){let a=i[n]||0,s=o[n]||0;if(a<s)return 1;if(a>s)return-1}return 0}var Q=async({projectDir:e,projectName:t,noInstall:i,flavour:o})=>{if(j.existsSync(e))if(j.readdirSync(e).length===0)t!=="."&&r.log.info(`${c.cyan.bold(t)} exists but is empty, continuing... `);else{let l=await r.select({message:`${c.redBright.bold("Warning:")} ${c.cyan.bold(t)} already exists and isn't empty. How would you like to proceed?`,options:[{label:"Abort installation (recommended)",value:"abort"},{label:"Clear the directory and continue installation",value:"clear"},{label:"Continue installation and overwrite conflicting files",value:"overwrite"}]});(r.isCancel(l)||l==="abort")&&(r.log.error("Aborting installation..."),process.exit(1));let h=await r.confirm({message:`Are you sure you want to ${l==="clear"?"clear the directory":"overwrite conflicting files"}?`,initialValue:!1});(r.isCancel(h)||!h)&&(r.log.error("Aborting installation..."),process.exit(1)),l==="clear"&&(r.log.info(` Emptying ${c.cyan.bold(t)} and creating CEN app.. `),j.emptyDirSync(e))}let n=i,a=[],s=[];if(r.log.info(`Checking for dependencies... `),o!=="go"&&o!=="java"&&(await $e()?r.log.success(`${c.green("uv is installed")}`):(s.push("uv"),r.log.error(c.red("\u274C uv is not installed")),r.log.message(c.cyan.bold("Install uv: https://docs.astral.sh/uv/getting-started/installation/")))),await Se()?(r.log.success(`${c.green("Docker CLI is installed")}`),await Ee()?r.log.success(`${c.green("Docker Compose is installed")}`):(a.push("Docker Compose"),r.log.warn(c.yellow("Docker Compose is not available")),r.log.message(c.cyan.bold(`Install Docker Compose with Homebrew: brew install docker-compose `)),r.log.message(c.cyan.bold("Install Docker Runtime (colima): https://github.com/abiosoft/colima/ ")))):(a.push("Docker CLI"),r.log.warn(c.yellow("Docker CLI is not installed")),r.log.message(c.cyan.bold(`Install Docker with Homebrew: brew install docker `)),r.log.message(c.cyan.bold("Install Docker Runtime (colima): https://github.com/abiosoft/colima/ "))),o!=="go"&&o!=="java"&&(await Te()?r.log.success(`${c.green("Python 3.10, 3.11, or 3.12 is installed")}`):(s.push("Python 3.10+"),r.log.error(c.red("\u274C You need Python 3.10, 3.11, or 3.12 installed to use this template")),r.log.message(c.cyan.bold("Install Python: https://www.python.org/downloads/")))),o!=="backend-only"&&o!=="backend-only-no-db"&&(await De()?r.log.success(`${c.green("Node.js 20.x or higher is installed")}`):(s.push("Node.js 20+"),r.log.error(c.red("\u274C You need Node.js 20.x or higher to use this template")),r.log.message(c.cyan.bold("Install Node.js: https://nodejs.org/")))),a.length>0||s.length>0){let l=`Missing dependencies: ${[...s,...a].join(", ")}`;r.log.warn(c.yellow(l));let f=await r.confirm({message:"Would you like to continue anyway?",initialValue:s.length===0});(r.isCancel(f)||!f)&&(r.log.error(c.red("Aborting installation...")),process.exit(1)),s.length>0&&(n=!0)}return{noInstall:n,missingDependencies:[...s,...a]}},$e=async()=>{try{let{stdout:e}=await x("uv",["--version"]);return e!==""}catch{return!1}},Se=async()=>{try{let{stdout:e}=await x("docker",["--version"]);return e!==""}catch{return!1}},Ee=async()=>{let e=!1;try{let{stdout:t}=await x("docker",["compose","version"]);e=t!==""}catch{e=!1}if(!e)try{let{stdout:t}=await x("docker-compose",["version"]);e=t!==""}catch{e=!1}return e},Te=async()=>{let e=["3.10","3.11","3.12"],t=await q();return t?t.map(o=>o.version?.match(/(\d+\.\d+\.\d+)$/)?.[1]).filter(o=>o!==void 0).some(o=>e.some(n=>o.startsWith(n))):!1},De=async()=>{try{let{stdout:e}=await x("node",["--version"]),t=e?.slice(1).split(".")[0];return t?parseInt(t)>=20:!1}catch{return!1}};import*as ne from"@clack/prompts";import Re from"chalk";import L from"path";import*as g from"@clack/prompts";import C from"chalk";import{execa as b}from"execa";import Ve from"fs-extra";import ee from"path";import*as I from"@clack/prompts";import{execa as G}from"execa";var Oe=async(e,t)=>{let i=I.spinner();switch(e){case"npm":i.start("Installing dependencies with npm..."),await G(e,["install"],{cwd:t,stderr:"inherit"}),i.stop();break;case"pnpm":i.start("Running pnpm install...");let o=G(e,["install"],{cwd:t,stdout:"pipe"});await new Promise((a,s)=>{o.stdout?.on("data",d=>{let l=d.toString();l.includes("Progress")&&i.message(l.includes("|")?l.split(" | ")[1]??"":l)}),o.on("error",d=>s(d)),o.on("close",()=>a())}),i.stop();break;case"yarn":i.start("Running yarn...");let n=G(e,[],{cwd:t,stdout:"pipe"});await new Promise((a,s)=>{n.stdout?.on("data",d=>{i.message(d.toString())}),n.on("error",d=>s(d)),n.on("close",()=>a())}),i.stop();break}},Z=async({frontendDir:e})=>{I.log.info("Installing frontend dependencies...");let t=w();await Oe(t,e),I.log.success(`Successfully installed dependencies! `)};var te=async({projectName:e,flavour:t,backendDir:i,frontendDir:o,projectDir:n,noInstall:a})=>{let s=g.spinner();s.start("Cloning full-stack-cen-template repository..."),await b("git",["clone","-b",t,V,n],{cwd:A,stdio:"inherit"}),s.stop(),g.log.success(`Successfully cloned ${C.green.bold("full-stack-cen-template")} repository `),g.log.info("Setting up git...");try{await b("git",["branch","-m","main"],{cwd:n}),g.log.success(`Successfully renamed branch to ${C.green.bold("main")}`)}catch(l){g.log.error(`Failed to rename branch to main: ${l}`)}try{await b("git",["remote","remove","origin"],{cwd:n}),g.log.success(`Successfully removed git remote ${C.green.bold("origin")}`)}catch(l){g.log.error(`Failed to remove git remote origin: ${l}`)}try{await b("git",["remote","add","upstream",V],{cwd:n}),g.log.success(`Successfully added ${C.green.bold("upstream")} remote`)}catch(l){g.log.error(`Failed to add upstream remote: ${l}`)}g.log.success(`Successfully setup git `);let d=ee.join(n,".env");if(Ve.copySync(ee.join(n,".env.example"),d),a){g.log.info("Skipping dependencies installation");return}if(t==="backend-only"||t==="backend-only-no-db"||t==="main"||t==="oauth-proxy"||t==="main-custom-ui"||t==="oauth-proxy-custom-ui"){g.log.info("Preparing Python environment...");let l=["3.12","3.11","3.10"],f=null;for(let h of l)if(await b(`python${h}`,["--version"]).then(()=>!0).catch(()=>!1)){f=h;break}if(!f){g.log.error("Python 3.10 or higher is not installed. Please install Python 3.10+ and follow the Backend README.");return}s.start(`Creating virtual environment with Python ${f}...`);try{await b("uv",["venv","--python",`python${f}`],{cwd:i,stdio:"inherit"}),await b("uv",["sync"],{cwd:i,stdio:"inherit"}),s.stop(),g.log.success(`Successfully installed ${C.green.bold("python")} requirements `)}catch{s.stop(),g.log.error("Failed to setup Python environment. Please check the Backend README for manual setup.")}}t!=="backend-only"&&t!=="backend-only-no-db"&&await Z({frontendDir:o})};var oe=async({projectName:e,noInstall:t,flavour:i})=>{let o=w(),n=L.resolve(process.cwd(),e),a=L.resolve(n,"frontend"),s=L.resolve(n,"backend");t||ne.log.info(` Using: ${Re.cyan.bold(o)} `);let{noInstall:d,missingDependencies:l}=await Q({projectDir:n,projectName:e,noInstall:t,flavour:i});return t=d,await te({backendDir:s,frontendDir:a,projectDir:n,projectName:e,noInstall:t,flavour:i}),{frontendDir:a,backendDir:s,projectDir:n,missingDependencies:l}};import*as v from"@clack/prompts";import u from"chalk";var ie=({projectName:e=N,flavour:t,noInstall:i,missingDependencies:o})=>{v.log.info(`${u.bold.green("All done!")} \u{1F389} `);let n=`${u.bold.cyan("Note:")} `;n+=`${u.cyan(`The base template has been registered as upstream, so that you can pull updates later on. Add your own remote as origin and pull updates from upstream:`)} ${u.cyan("git remote add origin <your-remote-url>")} ${u.cyan("git pull --no-commit upstream "+t)}`,v.log.info(n);let a=`${u.bold.cyan("Next steps:")} `;o.length>0&&(a+=` ${u.yellow("Install missing dependencies:")} ${o.join(", ")} `),(t==="oauth-proxy"||t==="oauth-proxy-custom-ui")&&(a+=` ${u.cyan("Get an AppID Instance (see development.md) and put the credentials in .env")} `),a+=` ${u.cyan("cd")} ${e} `,a+=` ${u.cyan("docker compose watch")} `,v.log.info(a),v.log.message(""),v.outro(`${u.bold.green("Have fun building!")} \u{1F680}`)};import ae from"path";var se=e=>{let t=e.split("/"),i=t[t.length-1];if(i==="."){let a=ae.resolve(process.cwd());i=ae.basename(a)}let o=t.findIndex(a=>a.startsWith("@"));t.findIndex(a=>a.startsWith("@"))!==-1&&(i=t.slice(o).join("/"));let n=t.filter(a=>!a.startsWith("@")).join("/");return[i,n]};import Fe from"gradient-string";var je={blue:"#add7ff",cyan:"#89ddff",green:"#5de4c7",magenta:"#fae4fc",red:"#d0679d",yellow:"#fffac2"},re=()=>{let e=Fe(Object.values(je)),t=w();(t==="yarn"||t==="pnpm")&&console.log(""),console.log(e.multiline(z))};var Ge=async()=>{let e=await J();re(),e&&B(e);let{appName:t,flavour:i,flags:{noGit:o,noInstall:n}}=await X(),[a,s]=se(t),{projectDir:d,frontendDir:l,missingDependencies:f}=await oe({projectName:s,noInstall:n,flavour:i});if(i!=="backend-only"&&i!=="backend-only-no-db"){let h=le.readJSONSync(ce.join(l,"package.json"));h.name=a,h.ct3aMetadata={initVersion:k()},le.writeJSONSync(ce.join(l,"package.json"),h,{spaces:2})}o||await W(d),ie({projectName:s,noInstall:n,missingDependencies:f,flavour:i}),process.exit(0)};Ge().catch(e=>{S.log.error("Aborting installation..."),e instanceof Error?S.log.error(e.message):(S.log.error("An unknown error has occurred. Please open an issue on github with the below:"),console.log(e)),process.exit(1)}); //# sourceMappingURL=index.js.map