UNPKG

jirehgrp

Version:

Scaffold frontend apps from Jireh templates (Next.js, React+Vite, Vanilla, etc.)

242 lines (241 loc) 9.88 kB
#!/usr/bin/env node // src/index.ts import ora from "ora"; import fs from "node:fs"; import kleur from "kleur"; import path from "node:path"; import prompts from "prompts"; import minimist from "minimist"; import { execSync } from "node:child_process"; import { ask } from "./prompts.js"; import { registry } from "./registry.js"; import { writeReadme } from "./writeReadme.js"; import { copyTemplate } from "./fetchTemplate.js"; import { finalizeProject } from "./postInstall.js"; import { buildTreeString, writeTreeToFile } from "./tree.js"; const argv = minimist(process.argv.slice(2), { string: ["template", "name", "tag", "dir", "pm"], boolean: ["install", "git", "yes", "tree", "readme"], default: { install: undefined, git: undefined, tree: false, readme: false }, }); function getProjectName(dir) { try { const pkgJsonPath = path.join(dir, "package.json"); if (fs.existsSync(pkgJsonPath)) { const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")); if (pkg.name) return pkg.name; } } catch { // ignore error and fallback below } return path.basename(dir); } async function handleDirConflict(dir) { if (!fs.existsSync(dir)) return dir; if (fs.readdirSync(dir).length === 0) return dir; const response = await prompts({ type: "select", name: "action", message: `Target directory '${dir}' exists and is not empty. What do you want to do?`, choices: [ { title: "Overwrite existing folder", value: "overwrite" }, { title: "Rename new project folder", value: "rename" }, { title: "Backup existing folder", value: "backup" }, { title: "Cancel", value: "cancel" }, ], }); switch (response.action) { case "overwrite": fs.rmSync(dir, { recursive: true, force: true }); fs.mkdirSync(dir, { recursive: true }); return dir; case "rename": { const renameRes = await prompts({ type: "text", name: "newName", message: "Enter new folder name:", initial: dir + "-new", }); if (!renameRes.newName) return null; const newDir = path.resolve(process.cwd(), renameRes.newName); if (fs.existsSync(newDir)) { console.error(kleur.red(`Folder '${newDir}' already exists. Aborting.`)); return null; } return newDir; } case "backup": { const backupName = dir + "-" + new Date().toISOString().replace(/[:.]/g, "-") + "-backup"; fs.renameSync(dir, backupName); fs.mkdirSync(dir, { recursive: true }); console.log(kleur.green(`Backed up existing folder to '${backupName}'.`)); return dir; } case "cancel": default: console.log(kleur.yellow("Operation cancelled by user.")); return null; } } (async () => { if (argv.tree) { const dirToPrint = path.resolve(process.cwd(), argv.dir || "."); const rootName = getProjectName(dirToPrint); const fullTreeColored = buildTreeString(dirToPrint, "", { color: true }); console.log(kleur.bold(`\nProject structure for: ${dirToPrint}\n`)); const treeWithRoot = `${rootName}/\n${fullTreeColored}`; const treeLines = treeWithRoot.trim().split("\n"); const snippetLimit = 30; if (treeLines.length > snippetLimit) { console.log(treeLines.slice(0, snippetLimit).join("\n")); console.log(kleur.gray(`\n...snipped ${treeLines.length - snippetLimit} lines...\n`)); } else { console.log(treeWithRoot); } let writeJson; if (argv.json) writeJson = true; else if (argv.txt) writeJson = false; else { const response = await prompts({ type: "select", name: "format", message: "Choose output file format:", choices: [ { title: "TXT (default)", value: false }, { title: "JSON", value: true }, ], initial: 0, }); writeJson = typeof response.format === "boolean" ? response.format : false; } const filePath = writeTreeToFile(dirToPrint, { color: false, json: writeJson, rootName }); console.log(kleur.green(`Project structure mapped and written.`)); console.log(kleur.green(`Check it out at: ${filePath}\n`)); process.exit(0); } if (argv.readme) { const response = await prompts([ { type: 'text', name: 'projectName', message: 'Project name:', initial: path.basename(process.cwd()) }, { type: 'text', name: 'description', message: 'Project description:', initial: 'A project created with jirehgrp-CLI' }, { type: 'text', name: 'packageName', message: 'Package name:', initial: path.basename(process.cwd()) }, { type: 'text', name: 'usage', message: 'Usage command:', initial: 'npm run dev' }, { type: 'text', name: 'license', message: 'License:', initial: 'MIT' }, ]); await writeReadme(process.cwd(), { projectName: response.projectName, description: response.description, packageName: response.packageName, usage: response.usage, license: response.license, includeGeneratedBy: true, }); console.log(kleur.green(`✔️ README.md written to ${process.cwd()}`)); process.exit(0); } const answers = await ask({ name: argv.name, install: argv.install, git: argv.git, mainTemplateKey: argv.template, subTemplateKey: argv.template, }); const dest = path.resolve(process.cwd(), argv.dir ?? answers.name); let targetDir = dest; if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) { if (!argv.yes) { const resolvedDir = await handleDirConflict(targetDir); if (!resolvedDir) process.exit(1); targetDir = resolvedDir; } else { fs.rmSync(targetDir, { recursive: true, force: true }); fs.mkdirSync(targetDir, { recursive: true }); } } else { fs.mkdirSync(targetDir, { recursive: true }); } const entry = registry[answers.subTemplateKey]; const repoRef = argv.tag ? `${entry.repo.split("#")[0]}#${argv.tag}` : entry.repo; const spin = ora(`Fetching ${entry.label} ...`).start(); try { await copyTemplate(repoRef, entry.subdir, targetDir); spin.succeed("Template copied"); await writeReadme(targetDir, { projectName: answers.name, description: `A project created with ${entry.label} template.`, }); await finalizeProject(targetDir, answers.name, { install: fs.existsSync(path.join(targetDir, "package.json")) ? !!answers.install : false, git: !!answers.git, pm: argv.pm, }); if (!fs.existsSync(path.join(targetDir, ".git"))) { const gitInitAnswer = await prompts({ type: "confirm", name: "init", message: "Git repository not found. Initialize git?", initial: true, }); if (gitInitAnswer.init) { try { execSync("git init", { cwd: targetDir, stdio: "inherit" }); console.log(kleur.green("✔️ Git repository initialized")); } catch (err) { console.error(kleur.red("Failed to initialize git:"), err); } } } const showTreeAnswer = await prompts({ type: "confirm", name: "show", message: "Do you want to display the project tree?", initial: true, }); // After finalizeProject() and git initialization const treeFilePath = writeTreeToFile(targetDir, { color: false, json: false, rootName: getProjectName(targetDir) }); console.log(kleur.green(`✔️ Project structure saved to ${treeFilePath}`)); if (showTreeAnswer.show) { const rootName = getProjectName(targetDir); const tree = buildTreeString(targetDir, "", { color: true }); console.log(kleur.bold(`\nProject structure for: ${targetDir}\n`)); console.log(`${rootName}/\n${tree}`); } console.log(`\n${kleur.bold("Done!")} Created ${kleur.cyan(answers.name)} at ${kleur.gray(targetDir)}\n`); console.log(kleur.bold("Next steps:")); console.log(` cd ${answers.name}`); if (fs.existsSync(path.join(targetDir, "package.json"))) { const pm = (argv.pm || "npm"); const commands = { npm: ["npm install", "npm run dev"], yarn: ["yarn install", "yarn dev"], pnpm: ["pnpm install", "pnpm dev"], bun: ["bun install", "bun dev"], }; const [installCmd, devCmd] = commands[pm] || commands.npm; if (!answers.install) console.log(` ${installCmd}`); console.log(` ${devCmd}\n`); } else { console.log(` Open ${kleur.cyan("index.html")} in your browser.`); console.log(` Or serve locally:\n - live-server\n - python3 -m http.server\n`); } } catch (e) { spin.fail("Failed to fetch template"); console.error(e); process.exit(1); } process.exit(0); })();