|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +import { execSync } from "node:child_process"; |
| 4 | +import { existsSync, readdirSync, rmSync, writeFileSync } from "node:fs"; |
| 5 | +import { basename, join, resolve } from "node:path"; |
| 6 | +import * as p from "@clack/prompts"; |
| 7 | + |
| 8 | +const REPO_URL = "https://github.com/first-fluke/fullstack-starter.git"; |
| 9 | +const TITLE = "Fullstack Starter"; |
| 10 | +const DESCRIPTION = "Production-ready fullstack monorepo template"; |
| 11 | + |
| 12 | +async function main() { |
| 13 | + console.log(); |
| 14 | + p.intro(`${TITLE} - ${DESCRIPTION}`); |
| 15 | + |
| 16 | + const args = process.argv.slice(2).filter((arg) => !arg.startsWith("-")); |
| 17 | + const flags = process.argv.slice(2).filter((arg) => arg.startsWith("-")); |
| 18 | + const noInstall = flags.includes("--no-install"); |
| 19 | + const noGit = flags.includes("--no-git"); |
| 20 | + |
| 21 | + let projectDir = args[0]; |
| 22 | + |
| 23 | + if (!projectDir) { |
| 24 | + const result = await p.text({ |
| 25 | + message: "Where should we create your project?", |
| 26 | + placeholder: "./my-app", |
| 27 | + validate: (value) => { |
| 28 | + if (!value) return "Please enter a directory."; |
| 29 | + }, |
| 30 | + }); |
| 31 | + |
| 32 | + if (p.isCancel(result)) { |
| 33 | + p.cancel("Cancelled."); |
| 34 | + process.exit(0); |
| 35 | + } |
| 36 | + |
| 37 | + projectDir = result; |
| 38 | + } |
| 39 | + |
| 40 | + const targetDir = resolve(process.cwd(), projectDir); |
| 41 | + const projectName = basename(targetDir); |
| 42 | + |
| 43 | + if (existsSync(targetDir) && readdirSync(targetDir).length > 0) { |
| 44 | + const overwrite = await p.confirm({ |
| 45 | + message: `Directory "${projectName}" is not empty. Overwrite?`, |
| 46 | + initialValue: false, |
| 47 | + }); |
| 48 | + |
| 49 | + if (p.isCancel(overwrite) || !overwrite) { |
| 50 | + p.cancel("Cancelled."); |
| 51 | + process.exit(0); |
| 52 | + } |
| 53 | + |
| 54 | + rmSync(targetDir, { recursive: true, force: true }); |
| 55 | + } |
| 56 | + |
| 57 | + const spinner = p.spinner(); |
| 58 | + spinner.start("Cloning template from first-fluke/fullstack-starter"); |
| 59 | + |
| 60 | + try { |
| 61 | + execSync(`git clone --depth 1 ${REPO_URL} "${targetDir}"`, { stdio: "ignore" }); |
| 62 | + // Remove .git from cloned template |
| 63 | + rmSync(join(targetDir, ".git"), { recursive: true, force: true }); |
| 64 | + spinner.stop("Template cloned."); |
| 65 | + } catch { |
| 66 | + spinner.stop("Failed to clone template."); |
| 67 | + p.log.error("Check your network connection and try again."); |
| 68 | + process.exit(1); |
| 69 | + } |
| 70 | + |
| 71 | + // Clean up template-specific files |
| 72 | + spinner.start("Cleaning up template files"); |
| 73 | + const filesToRemove = ["CHANGELOG.md", "version.txt", ".github", ".agents", ".claude", ".serena"]; |
| 74 | + |
| 75 | + for (const file of filesToRemove) { |
| 76 | + const filePath = join(targetDir, file); |
| 77 | + if (existsSync(filePath)) { |
| 78 | + rmSync(filePath, { recursive: true, force: true }); |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + writeFileSync( |
| 83 | + join(targetDir, "README.md"), |
| 84 | + `# ${projectName}\n\nCreated with [Fullstack Starter](https://github.com/first-fluke/fullstack-starter).\n`, |
| 85 | + ); |
| 86 | + spinner.stop("Cleaned up."); |
| 87 | + |
| 88 | + if (!noGit) { |
| 89 | + spinner.start("Initializing git repository"); |
| 90 | + try { |
| 91 | + execSync("git init", { cwd: targetDir, stdio: "ignore" }); |
| 92 | + execSync("git add -A", { cwd: targetDir, stdio: "ignore" }); |
| 93 | + execSync('git commit -m "init: scaffold from fullstack-starter"', { |
| 94 | + cwd: targetDir, |
| 95 | + stdio: "ignore", |
| 96 | + }); |
| 97 | + spinner.stop("Git initialized."); |
| 98 | + } catch { |
| 99 | + spinner.stop("Git initialization skipped."); |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + if (!noInstall) { |
| 104 | + spinner.start("Installing dependencies (this may take a while)"); |
| 105 | + try { |
| 106 | + execSync("mise install", { cwd: targetDir, stdio: "ignore", timeout: 300_000 }); |
| 107 | + spinner.stop("Dependencies installed."); |
| 108 | + } catch { |
| 109 | + spinner.stop("Auto-install skipped. Run `mise install` manually."); |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + const steps = [`cd ${projectDir}`, ...(noInstall ? ["mise install"] : []), "mise dev"]; |
| 114 | + |
| 115 | + p.note(steps.join("\n"), "Next steps"); |
| 116 | + p.outro("Happy hacking!"); |
| 117 | +} |
| 118 | + |
| 119 | +main().catch((error) => { |
| 120 | + console.error(error); |
| 121 | + process.exit(1); |
| 122 | +}); |
0 commit comments