the best lightweight web dev stack built on bun
1#!/usr/bin/env bun
2/**
3 * Tacy Stack CLI
4 * Interactive project scaffolder
5 */
6
7import { $ } from "bun";
8import { existsSync, mkdirSync } from "node:fs";
9import { join } from "node:path";
10import * as p from "@clack/prompts";
11import { setTimeout } from "node:timers/promises";
12import { version } from "./package.json";
13
14async function main() {
15 console.clear();
16
17 p.intro(`🥞 Tacy Stack Generator @ ${version}`);
18
19 // Get project name
20 const projectName = await p.text({
21 message: "What is your project name?",
22 placeholder: "my-app",
23 validate(value) {
24 if (!value) return "Project name is required";
25 if (!/^[a-z0-9-_]+$/i.test(value)) {
26 return "Project name can only contain letters, numbers, hyphens, and underscores";
27 }
28 const targetDir = join(process.cwd(), value);
29 if (existsSync(targetDir)) {
30 return `Directory "${value}" already exists!`;
31 }
32 },
33 });
34
35 if (p.isCancel(projectName)) {
36 p.cancel("Operation cancelled");
37 process.exit(0);
38 }
39
40 const targetDir = join(process.cwd(), projectName as string);
41 const templateDir = import.meta.dir;
42
43 const s = p.spinner();
44
45 try {
46 // Create directory
47 s.start("Creating project directory");
48 mkdirSync(targetDir, { recursive: true });
49 await setTimeout(200);
50 s.stop("Created project directory");
51
52 // Copy template files
53 s.start("Copying template files");
54 await $`cp -r ${templateDir}/* ${targetDir}/`.quiet();
55
56 // Copy dotfiles explicitly
57 const dotfiles = [".env.example", ".gitignore", ".gitattributes"];
58 for (const dotfile of dotfiles) {
59 const source = join(templateDir, dotfile);
60 const dest = join(targetDir, dotfile);
61 if (existsSync(source)) {
62 await Bun.write(dest, Bun.file(source));
63 }
64 }
65
66 // Copy .github directory if it exists
67 const githubDir = join(templateDir, ".github");
68 if (existsSync(githubDir)) {
69 await $`cp -r ${githubDir} ${targetDir}/.github`.quiet();
70 }
71
72 await setTimeout(200);
73 s.stop("Copied template files");
74
75 // Remove CLI and template files
76 s.start("Cleaning up template files");
77 const filesToRemove = [
78 "cli.ts",
79 "TEMPLATE.md",
80 "TEMPLATE_SETUP_SUMMARY.md",
81 "CLI_SUMMARY.md",
82 "PUBLISHING.md",
83 "template.toml",
84 ".github/TEMPLATE_SETUP.md",
85 ];
86
87 for (const file of filesToRemove) {
88 const filePath = join(targetDir, file);
89 if (existsSync(filePath)) {
90 await $`rm -rf ${filePath}`.quiet();
91 }
92 }
93 await setTimeout(200);
94 s.stop("Cleaned up template files");
95
96 // Update package.json
97 s.start("Configuring package.json");
98 const packageJsonPath = join(targetDir, "package.json");
99 const packageJson = await Bun.file(packageJsonPath).json();
100 packageJson.name = projectName;
101 packageJson.version = "0.1.0";
102 delete packageJson.bin;
103 // Remove @clack/prompts from dependencies since it's only for the CLI
104 if (packageJson.dependencies?.["@clack/prompts"]) {
105 delete packageJson.dependencies["@clack/prompts"];
106 }
107 await Bun.write(
108 packageJsonPath,
109 JSON.stringify(packageJson, null, "\t") + "\n",
110 );
111 await setTimeout(200);
112 s.stop("Configured package.json");
113
114 // Initialize git
115 s.start("Initializing git repository");
116 await $`cd ${targetDir} && git init`.quiet();
117 await setTimeout(200);
118 s.stop("Initialized git repository");
119
120 // Create .env
121 s.start("Creating .env file");
122 await $`cd ${targetDir} && cp .env.example .env`.quiet();
123 await setTimeout(200);
124 s.stop("Created .env file");
125
126 // Install dependencies
127 s.start("Installing dependencies");
128 await $`cd ${targetDir} && bun install`.quiet();
129 s.stop("Installed dependencies");
130
131 // Setup database
132 s.start("Setting up database");
133 await $`cd ${targetDir} && bun run db:push`.quiet();
134 s.stop("Set up database");
135 } catch (error) {
136 s.stop("Failed");
137 p.cancel(
138 `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
139 );
140 process.exit(1);
141 }
142
143 p.outro("🎉 Project created successfully!");
144
145 p.note(`cd ${projectName}\nbun dev`, "Next steps");
146}
147
148main().catch((error) => {
149 console.error(error);
150 process.exit(1);
151});