chore: Holy refactor batmann 6ea331cb
Steve · 2025-05-16 15:05 5 file(s) · +359 −359
src/index.ts +1 −359
1 1
#!/usr/bin/env node
2 2
// @ts-ignore: Shebang line
3 3
4 -
import fs from "fs-extra";
5 -
import path from "node:path";
6 -
import { fileURLToPath } from "node:url";
7 -
import prompts from "prompts";
8 4
import { program } from "commander";
9 5
import chalk from "chalk";
10 -
import ora from "ora";
11 -
import { execa } from "execa";
12 -
import degit from "degit";
13 -
import figlet from "figlet";
14 -
import {
15 -
	defaultTemplate,
16 -
	shadcnTemplate,
17 -
	tailwindTemplate,
18 -
	honoRpcTemplate,
19 -
} from "./utils/templates";
20 -
21 -
const __filename = fileURLToPath(import.meta.url);
22 -
const __dirname = path.dirname(__filename);
23 -
24 -
// GitHub repository for the template
25 -
const DEFAULT_REPO = "stevedylandev/bhvr";
26 -
27 -
interface TemplateInfo {
28 -
	branch: string;
29 -
	description: string;
30 -
}
31 -
32 -
const TEMPLATES: Record<string, TemplateInfo> = {
33 -
	default: {
34 -
		branch: "main",
35 -
		description: "Basic setup with Bun, Hono, Vite and React",
36 -
	},
37 -
	tailwind: { branch: "tailwindcss", description: "Basic setup + TailwindCSS" },
38 -
	shadcn: {
39 -
		branch: "shadcn-ui",
40 -
		description: "Basic setup + TailwindCSS + shadcn/ui",
41 -
	},
42 -
};
43 -
44 -
interface ProjectOptions {
45 -
	yes?: boolean;
46 -
	typescript?: boolean;
47 -
	repo?: string;
48 -
	template?: string;
49 -
	branch?: string;
50 -
	rpc?: boolean;
51 -
}
52 -
53 -
function displayBanner() {
54 -
	try {
55 -
		const text = figlet.textSync("bhvr", {
56 -
			font: "Standard", // Use the Standard font which is more commonly available
57 -
			horizontalLayout: "default",
58 -
			verticalLayout: "default",
59 -
			width: 80,
60 -
			whitespaceBreak: true,
61 -
		});
62 -
63 -
		console.log("\n");
64 -
		console.log(chalk.yellowBright(text));
65 -
	} catch (error) {
66 -
		// Fallback in case figlet fails for any reason
67 -
		console.log("\n");
68 -
		console.log(chalk.yellowBright("B H V R"));
69 -
		console.log(chalk.yellow("=========="));
70 -
	}
71 -
72 -
	console.log(`\n${chalk.cyan("🦫 Lets build 🦫")}\n`);
73 -
	console.log(`${chalk.blue("https://github.com/stevedylandev/bhvr")}\n`);
74 -
}
6 +
import { displayBanner, createProject, DEFAULT_REPO } from "./utils";
75 7
76 8
program
77 9
	.name("create-bhvr")
122 54
	});
123 55
124 56
program.parse();
125 -
126 -
interface ProjectResult {
127 -
	projectName: string;
128 -
	gitInitialized: boolean;
129 -
	dependenciesInstalled: boolean;
130 -
	template: string;
131 -
}
132 -
133 -
async function createProject(
134 -
	projectDirectory: string,
135 -
	options: ProjectOptions,
136 -
): Promise<ProjectResult | null> {
137 -
	let projectName = projectDirectory;
138 -
139 -
	if (!projectName && !options.yes) {
140 -
		const response = await prompts({
141 -
			type: "text",
142 -
			name: "projectName",
143 -
			message: "What is the name of your project?",
144 -
			initial: "my-bhvr-app",
145 -
		});
146 -
147 -
		if (!response.projectName) {
148 -
			console.log(chalk.yellow("Project creation cancelled."));
149 -
			return null;
150 -
		}
151 -
152 -
		projectName = response.projectName;
153 -
	} else if (!projectName) {
154 -
		projectName = "my-bhvr-app";
155 -
	}
156 -
157 -
	let templateChoice = options.template || "default";
158 -
159 -
	if (!options.yes && !options.branch) {
160 -
		const templateChoices = Object.keys(TEMPLATES).map((key) => ({
161 -
			title: `${key} (${TEMPLATES[key]?.description})`,
162 -
			value: key,
163 -
		}));
164 -
165 -
		const templateResponse = await prompts({
166 -
			type: "select",
167 -
			name: "template",
168 -
			message: "Select a template:",
169 -
			choices: templateChoices,
170 -
			initial: 0,
171 -
		});
172 -
173 -
		if (templateResponse.template === undefined) {
174 -
			console.log(chalk.yellow("Project creation cancelled."));
175 -
			return null;
176 -
		}
177 -
178 -
		templateChoice = templateResponse.template;
179 -
	}
180 -
181 -
	const projectPath = path.resolve(process.cwd(), projectName);
182 -
183 -
	if (fs.existsSync(projectPath)) {
184 -
		const files = fs.readdirSync(projectPath);
185 -
186 -
		if (files.length > 0 && !options.yes) {
187 -
			const { overwrite } = await prompts({
188 -
				type: "confirm",
189 -
				name: "overwrite",
190 -
				message: `The directory ${projectName} already exists and is not empty. Do you want to overwrite it?`,
191 -
				initial: false,
192 -
			});
193 -
194 -
			if (!overwrite) {
195 -
				console.log(chalk.yellow("Project creation cancelled."));
196 -
				return null;
197 -
			}
198 -
199 -
			await fs.emptyDir(projectPath);
200 -
		}
201 -
	}
202 -
203 -
	fs.ensureDirSync(projectPath);
204 -
205 -
	const repoPath = options.repo || DEFAULT_REPO;
206 -
	const templateConfig =
207 -
		TEMPLATES[templateChoice as keyof typeof TEMPLATES] || TEMPLATES.default;
208 -
	const branch = options.branch || (templateConfig?.branch ?? "main");
209 -
	const repoUrl = `${repoPath}#${branch}`;
210 -
211 -
	const spinner = ora("Downloading template...").start();
212 -
213 -
	try {
214 -
		const emitter = degit(repoUrl, {
215 -
			cache: false,
216 -
			force: true,
217 -
			verbose: false,
218 -
		});
219 -
220 -
		await emitter.clone(projectPath);
221 -
		spinner.succeed(
222 -
			`Template downloaded successfully (${templateChoice} template)`,
223 -
		);
224 -
225 -
		const pkgJsonPath = path.join(projectPath, "package.json");
226 -
		if (fs.existsSync(pkgJsonPath)) {
227 -
			const pkgJson = await fs.readJson(pkgJsonPath);
228 -
			pkgJson.name = projectName;
229 -
			await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
230 -
		}
231 -
232 -
		const gitDir = path.join(projectPath, ".git");
233 -
		if (fs.existsSync(gitDir)) {
234 -
			await fs.remove(gitDir);
235 -
			console.log(chalk.blue("Removed .git directory"));
236 -
		}
237 -
238 -
		let useRpc = options.rpc;
239 -
240 -
		if (!options.yes && !options.rpc) {
241 -
			const rpcResponse = await prompts({
242 -
				type: "confirm",
243 -
				name: "useRpc",
244 -
				message: "Use Hono RPC client for type-safe API communication?",
245 -
				initial: false,
246 -
			});
247 -
248 -
			if (rpcResponse.useRpc === undefined) {
249 -
				console.log(chalk.yellow("Project creation cancelled."));
250 -
				return null;
251 -
			}
252 -
253 -
			useRpc = rpcResponse.useRpc;
254 -
		}
255 -
256 -
		if (useRpc) {
257 -
			await patchFilesForRPC(projectPath, templateChoice);
258 -
		}
259 -
260 -
		let gitInitialized = false;
261 -
262 -
		if (!options.yes) {
263 -
			const gitResponse = await prompts({
264 -
				type: "confirm",
265 -
				name: "initGit",
266 -
				message: "Initialize a git repository?",
267 -
				initial: true,
268 -
			});
269 -
270 -
			if (gitResponse.initGit) {
271 -
				try {
272 -
					spinner.start("Initializing git repository...");
273 -
					await execa("git", ["init"], { cwd: projectPath });
274 -
					spinner.succeed("Git repository initialized");
275 -
					gitInitialized = true;
276 -
				} catch (err: unknown) {
277 -
					spinner.fail(
278 -
						"Failed to initialize git repository. Is git installed?",
279 -
					);
280 -
					if (err instanceof Error) {
281 -
						console.error(chalk.red("Git error:"), err.message);
282 -
					} else {
283 -
						console.error(chalk.red("Git error: Unknown error"));
284 -
					}
285 -
				}
286 -
			}
287 -
		} else {
288 -
			try {
289 -
				spinner.start("Initializing git repository...");
290 -
				await execa("git", ["init"], { cwd: projectPath });
291 -
				spinner.succeed("Git repository initialized");
292 -
				gitInitialized = true;
293 -
			} catch (err) {
294 -
				spinner.fail("Failed to initialize git repository. Is git installed?");
295 -
			}
296 -
		}
297 -
298 -
		let dependenciesInstalled = false;
299 -
300 -
		if (!options.yes) {
301 -
			const depsResponse = await prompts({
302 -
				type: "confirm",
303 -
				name: "installDeps",
304 -
				message: "Install dependencies?",
305 -
				initial: true,
306 -
			});
307 -
308 -
			if (depsResponse.installDeps) {
309 -
				spinner.start("Installing dependencies...");
310 -
				try {
311 -
					await execa("bun", ["install"], { cwd: projectPath });
312 -
					spinner.succeed("Dependencies installed with bun");
313 -
					dependenciesInstalled = true;
314 -
				} catch (bunErr) {
315 -
					try {
316 -
						spinner.text = "Installing dependencies with npm...";
317 -
						await execa("npm", ["install"], { cwd: projectPath });
318 -
						spinner.succeed("Dependencies installed with npm");
319 -
						dependenciesInstalled = true;
320 -
					} catch (npmErr) {
321 -
						spinner.fail("Failed to install dependencies.");
322 -
						console.log(
323 -
							chalk.yellow(
324 -
								"You can install them manually after navigating to the project directory.",
325 -
							),
326 -
						);
327 -
					}
328 -
				}
329 -
			}
330 -
		} else {
331 -
			spinner.start("Installing dependencies...");
332 -
			try {
333 -
				await execa("bun", ["install"], { cwd: projectPath });
334 -
				spinner.succeed("Dependencies installed with bun");
335 -
				dependenciesInstalled = true;
336 -
			} catch (bunErr) {
337 -
				try {
338 -
					spinner.text = "Installing dependencies with npm...";
339 -
					await execa("npm", ["install"], { cwd: projectPath });
340 -
					spinner.succeed("Dependencies installed with npm");
341 -
					dependenciesInstalled = true;
342 -
				} catch (npmErr) {
343 -
					spinner.fail(
344 -
						"Failed to install dependencies. You can install them manually later.",
345 -
					);
346 -
				}
347 -
			}
348 -
		}
349 -
350 -
		return {
351 -
			projectName,
352 -
			gitInitialized,
353 -
			dependenciesInstalled,
354 -
			template: templateChoice,
355 -
		};
356 -
	} catch (err) {
357 -
		spinner.fail("Failed to download template");
358 -
		throw err;
359 -
	}
360 -
}
361 -
362 -
async function patchFilesForRPC(
363 -
	projectPath: string,
364 -
	templateChoice: string,
365 -
): Promise<boolean> {
366 -
	const spinner = ora("Setting up RPC client...").start();
367 -
368 -
	try {
369 -
		// 1. Update client package.json to ensure hono client is installed
370 -
		const clientPkgPath = path.join(projectPath, "client", "package.json");
371 -
		const clientPkg = await fs.readJson(clientPkgPath);
372 -
373 -
		if (!clientPkg.dependencies.hono) {
374 -
			await execa("bun", ["install", "hono"], { cwd: projectPath });
375 -
		}
376 -
377 -
		await fs.writeJson(clientPkgPath, clientPkg, { spaces: 2 });
378 -
379 -
		// 2. Server modification for RPC export type
380 -
		const serverIndexPath = path.join(projectPath, "server", "src", "index.ts");
381 -
		await fs.writeFile(serverIndexPath, honoRpcTemplate, "utf8");
382 -
383 -
		// 3. Update App.tsx based on template selection using switch statement
384 -
		const appTsxPath = path.join(projectPath, "client", "src", "App.tsx");
385 -
386 -
		// Determine template content based on the template type
387 -
		let updatedAppContent: string;
388 -
389 -
		// Select template based on choice
390 -
		switch (templateChoice) {
391 -
			case "shadcn":
392 -
				updatedAppContent = shadcnTemplate;
393 -
				break;
394 -
			case "tailwind":
395 -
				updatedAppContent = tailwindTemplate;
396 -
				break;
397 -
			default:
398 -
				updatedAppContent = defaultTemplate;
399 -
				break;
400 -
		}
401 -
402 -
		await fs.writeFile(appTsxPath, updatedAppContent, "utf8");
403 -
		spinner.succeed("RPC client setup completed");
404 -
		return true;
405 -
	} catch (err: unknown) {
406 -
		spinner.fail("Failed to set up RPC client");
407 -
		if (err instanceof Error) {
408 -
			console.error(chalk.red("Error:"), err.message);
409 -
		} else {
410 -
			console.error(chalk.red("Error: Unknown error"));
411 -
		}
412 -
		return false;
413 -
	}
414 -
}
src/types.ts (added) +20 −0
1 +
export interface TemplateInfo {
2 +
	branch: string;
3 +
	description: string;
4 +
}
5 +
6 +
export interface ProjectOptions {
7 +
	yes?: boolean;
8 +
	typescript?: boolean;
9 +
	repo?: string;
10 +
	template?: string;
11 +
	branch?: string;
12 +
	rpc?: boolean;
13 +
}
14 +
15 +
export interface ProjectResult {
16 +
	projectName: string;
17 +
	gitInitialized: boolean;
18 +
	dependenciesInstalled: boolean;
19 +
	template: string;
20 +
}
src/utils/helpers.ts (added) +322 −0
1 +
import figlet from "figlet";
2 +
import chalk from "chalk";
3 +
import { execa } from "execa";
4 +
import ora from "ora";
5 +
import path from "node:path";
6 +
import fs from "fs-extra";
7 +
import {
8 +
	honoRpcTemplate,
9 +
	shadcnTemplate,
10 +
	tailwindTemplate,
11 +
	defaultTemplate,
12 +
	TEMPLATES,
13 +
} from "./templates";
14 +
import type { ProjectOptions, ProjectResult } from "../types";
15 +
import degit from "degit";
16 +
import prompts from "prompts";
17 +
18 +
export const DEFAULT_REPO = "stevedylandev/bhvr";
19 +
20 +
export function displayBanner() {
21 +
	try {
22 +
		const text = figlet.textSync("bhvr", {
23 +
			font: "Big",
24 +
			horizontalLayout: "default",
25 +
			verticalLayout: "default",
26 +
			width: 80,
27 +
			whitespaceBreak: true,
28 +
		});
29 +
30 +
		console.log("\n");
31 +
		console.log(chalk.yellowBright(text));
32 +
	} catch (error) {
33 +
		console.log("\n");
34 +
		console.log(chalk.yellowBright("B H V R"));
35 +
		console.log(chalk.yellow("=========="));
36 +
	}
37 +
38 +
	console.log(`\n${chalk.cyan("🦫 Lets build 🦫")}\n`);
39 +
	console.log(`${chalk.blue("https://github.com/stevedylandev/bhvr")}\n`);
40 +
}
41 +
42 +
export async function patchFilesForRPC(
43 +
	projectPath: string,
44 +
	templateChoice: string,
45 +
): Promise<boolean> {
46 +
	const spinner = ora("Setting up RPC client...").start();
47 +
48 +
	try {
49 +
		// 1. Update client package.json to ensure hono client is installed
50 +
		const clientPkgPath = path.join(projectPath, "client", "package.json");
51 +
		const clientPkg = await fs.readJson(clientPkgPath);
52 +
53 +
		if (!clientPkg.dependencies.hono) {
54 +
			await execa("bun", ["install", "hono"], { cwd: projectPath });
55 +
		}
56 +
57 +
		await fs.writeJson(clientPkgPath, clientPkg, { spaces: 2 });
58 +
59 +
		// 2. Server modification for RPC export type
60 +
		const serverIndexPath = path.join(projectPath, "server", "src", "index.ts");
61 +
		await fs.writeFile(serverIndexPath, honoRpcTemplate, "utf8");
62 +
63 +
		// 3. Update App.tsx based on template selection using switch statement
64 +
		const appTsxPath = path.join(projectPath, "client", "src", "App.tsx");
65 +
66 +
		// Determine template content based on the template type
67 +
		let updatedAppContent: string;
68 +
69 +
		// Select template based on choice
70 +
		switch (templateChoice) {
71 +
			case "shadcn":
72 +
				updatedAppContent = shadcnTemplate;
73 +
				break;
74 +
			case "tailwind":
75 +
				updatedAppContent = tailwindTemplate;
76 +
				break;
77 +
			default:
78 +
				updatedAppContent = defaultTemplate;
79 +
				break;
80 +
		}
81 +
82 +
		await fs.writeFile(appTsxPath, updatedAppContent, "utf8");
83 +
		spinner.succeed("RPC client setup completed");
84 +
		return true;
85 +
	} catch (err: unknown) {
86 +
		spinner.fail("Failed to set up RPC client");
87 +
		if (err instanceof Error) {
88 +
			console.error(chalk.red("Error:"), err.message);
89 +
		} else {
90 +
			console.error(chalk.red("Error: Unknown error"));
91 +
		}
92 +
		return false;
93 +
	}
94 +
}
95 +
96 +
export async function createProject(
97 +
	projectDirectory: string,
98 +
	options: ProjectOptions,
99 +
): Promise<ProjectResult | null> {
100 +
	let projectName = projectDirectory;
101 +
102 +
	if (!projectName && !options.yes) {
103 +
		const response = await prompts({
104 +
			type: "text",
105 +
			name: "projectName",
106 +
			message: "What is the name of your project?",
107 +
			initial: "my-bhvr-app",
108 +
		});
109 +
110 +
		if (!response.projectName) {
111 +
			console.log(chalk.yellow("Project creation cancelled."));
112 +
			return null;
113 +
		}
114 +
115 +
		projectName = response.projectName;
116 +
	} else if (!projectName) {
117 +
		projectName = "my-bhvr-app";
118 +
	}
119 +
120 +
	let templateChoice = options.template || "default";
121 +
122 +
	if (!options.yes && !options.branch) {
123 +
		const templateChoices = Object.keys(TEMPLATES).map((key) => ({
124 +
			title: `${key} (${TEMPLATES[key]?.description})`,
125 +
			value: key,
126 +
		}));
127 +
128 +
		const templateResponse = await prompts({
129 +
			type: "select",
130 +
			name: "template",
131 +
			message: "Select a template:",
132 +
			choices: templateChoices,
133 +
			initial: 0,
134 +
		});
135 +
136 +
		if (templateResponse.template === undefined) {
137 +
			console.log(chalk.yellow("Project creation cancelled."));
138 +
			return null;
139 +
		}
140 +
141 +
		templateChoice = templateResponse.template;
142 +
	}
143 +
144 +
	const projectPath = path.resolve(process.cwd(), projectName);
145 +
146 +
	if (fs.existsSync(projectPath)) {
147 +
		const files = fs.readdirSync(projectPath);
148 +
149 +
		if (files.length > 0 && !options.yes) {
150 +
			const { overwrite } = await prompts({
151 +
				type: "confirm",
152 +
				name: "overwrite",
153 +
				message: `The directory ${projectName} already exists and is not empty. Do you want to overwrite it?`,
154 +
				initial: false,
155 +
			});
156 +
157 +
			if (!overwrite) {
158 +
				console.log(chalk.yellow("Project creation cancelled."));
159 +
				return null;
160 +
			}
161 +
162 +
			await fs.emptyDir(projectPath);
163 +
		}
164 +
	}
165 +
166 +
	fs.ensureDirSync(projectPath);
167 +
168 +
	const repoPath = options.repo || DEFAULT_REPO;
169 +
	const templateConfig =
170 +
		TEMPLATES[templateChoice as keyof typeof TEMPLATES] || TEMPLATES.default;
171 +
	const branch = options.branch || (templateConfig?.branch ?? "main");
172 +
	const repoUrl = `${repoPath}#${branch}`;
173 +
	const spinner = ora("Downloading template...").start();
174 +
175 +
	try {
176 +
		const emitter = degit(repoUrl, {
177 +
			cache: false,
178 +
			force: true,
179 +
			verbose: false,
180 +
		});
181 +
182 +
		await emitter.clone(projectPath);
183 +
		spinner.succeed(
184 +
			`Template downloaded successfully (${templateChoice} template)`,
185 +
		);
186 +
187 +
		const pkgJsonPath = path.join(projectPath, "package.json");
188 +
		if (fs.existsSync(pkgJsonPath)) {
189 +
			const pkgJson = await fs.readJson(pkgJsonPath);
190 +
			pkgJson.name = projectName;
191 +
			await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
192 +
		}
193 +
194 +
		const gitDir = path.join(projectPath, ".git");
195 +
		if (fs.existsSync(gitDir)) {
196 +
			await fs.remove(gitDir);
197 +
			console.log(chalk.blue("Removed .git directory"));
198 +
		}
199 +
200 +
		let useRpc = options.rpc;
201 +
202 +
		if (!options.yes && !options.rpc) {
203 +
			const rpcResponse = await prompts({
204 +
				type: "confirm",
205 +
				name: "useRpc",
206 +
				message: "Use Hono RPC client for type-safe API communication?",
207 +
				initial: false,
208 +
			});
209 +
210 +
			if (rpcResponse.useRpc === undefined) {
211 +
				console.log(chalk.yellow("Project creation cancelled."));
212 +
				return null;
213 +
			}
214 +
215 +
			useRpc = rpcResponse.useRpc;
216 +
		}
217 +
218 +
		if (useRpc) {
219 +
			await patchFilesForRPC(projectPath, templateChoice);
220 +
		}
221 +
222 +
		let gitInitialized = false;
223 +
224 +
		if (!options.yes) {
225 +
			const gitResponse = await prompts({
226 +
				type: "confirm",
227 +
				name: "initGit",
228 +
				message: "Initialize a git repository?",
229 +
				initial: true,
230 +
			});
231 +
232 +
			if (gitResponse.initGit) {
233 +
				try {
234 +
					spinner.start("Initializing git repository...");
235 +
					await execa("git", ["init"], { cwd: projectPath });
236 +
					spinner.succeed("Git repository initialized");
237 +
					gitInitialized = true;
238 +
				} catch (err: unknown) {
239 +
					spinner.fail(
240 +
						"Failed to initialize git repository. Is git installed?",
241 +
					);
242 +
					if (err instanceof Error) {
243 +
						console.error(chalk.red("Git error:"), err.message);
244 +
					} else {
245 +
						console.error(chalk.red("Git error: Unknown error"));
246 +
					}
247 +
				}
248 +
			}
249 +
		} else {
250 +
			try {
251 +
				spinner.start("Initializing git repository...");
252 +
				await execa("git", ["init"], { cwd: projectPath });
253 +
				spinner.succeed("Git repository initialized");
254 +
				gitInitialized = true;
255 +
			} catch (err) {
256 +
				spinner.fail("Failed to initialize git repository. Is git installed?");
257 +
			}
258 +
		}
259 +
260 +
		let dependenciesInstalled = false;
261 +
262 +
		if (!options.yes) {
263 +
			const depsResponse = await prompts({
264 +
				type: "confirm",
265 +
				name: "installDeps",
266 +
				message: "Install dependencies?",
267 +
				initial: true,
268 +
			});
269 +
270 +
			if (depsResponse.installDeps) {
271 +
				spinner.start("Installing dependencies...");
272 +
				try {
273 +
					await execa("bun", ["install"], { cwd: projectPath });
274 +
					spinner.succeed("Dependencies installed with bun");
275 +
					dependenciesInstalled = true;
276 +
				} catch (bunErr) {
277 +
					try {
278 +
						spinner.text = "Installing dependencies with npm...";
279 +
						await execa("npm", ["install"], { cwd: projectPath });
280 +
						spinner.succeed("Dependencies installed with npm");
281 +
						dependenciesInstalled = true;
282 +
					} catch (npmErr) {
283 +
						spinner.fail("Failed to install dependencies.");
284 +
						console.log(
285 +
							chalk.yellow(
286 +
								"You can install them manually after navigating to the project directory.",
287 +
							),
288 +
						);
289 +
					}
290 +
				}
291 +
			}
292 +
		} else {
293 +
			spinner.start("Installing dependencies...");
294 +
			try {
295 +
				await execa("bun", ["install"], { cwd: projectPath });
296 +
				spinner.succeed("Dependencies installed with bun");
297 +
				dependenciesInstalled = true;
298 +
			} catch (bunErr) {
299 +
				try {
300 +
					spinner.text = "Installing dependencies with npm...";
301 +
					await execa("npm", ["install"], { cwd: projectPath });
302 +
					spinner.succeed("Dependencies installed with npm");
303 +
					dependenciesInstalled = true;
304 +
				} catch (npmErr) {
305 +
					spinner.fail(
306 +
						"Failed to install dependencies. You can install them manually later.",
307 +
					);
308 +
				}
309 +
			}
310 +
		}
311 +
312 +
		return {
313 +
			projectName,
314 +
			gitInitialized,
315 +
			dependenciesInstalled,
316 +
			template: templateChoice,
317 +
		};
318 +
	} catch (err) {
319 +
		spinner.fail("Failed to download template");
320 +
		throw err;
321 +
	}
322 +
}
src/utils/index.ts (added) +2 −0
1 +
export * from "./helpers";
2 +
export * from "./templates";
src/utils/templates.ts +14 −0
1 +
import type { TemplateInfo } from "../types";
2 +
3 +
export const TEMPLATES: Record<string, TemplateInfo> = {
4 +
	default: {
5 +
		branch: "main",
6 +
		description: "Basic setup with Bun, Hono, Vite and React",
7 +
	},
8 +
	tailwind: { branch: "tailwindcss", description: "Basic setup + TailwindCSS" },
9 +
	shadcn: {
10 +
		branch: "shadcn-ui",
11 +
		description: "Basic setup + TailwindCSS + shadcn/ui",
12 +
	},
13 +
};
14 +
1 15
export const honoRpcTemplate = `import { Hono } from "hono";
2 16
import { cors } from "hono/cors";
3 17
import type { ApiResponse } from "shared/dist";