Added support for Biome linter e5f1796c
Pedro Santana · 2025-07-05 13:10 4 file(s) · +420 −323
bun.lock +6 −6
4 4
    "": {
5 5
      "name": "create-bhvr",
6 6
      "dependencies": {
7 -
        "chalk": "^5.3.0",
7 +
        "chalk": "^5.4.1",
8 8
        "commander": "^11.1.0",
9 9
        "degit": "^2.8.4",
10 -
        "execa": "^7.1.1",
10 +
        "execa": "^7.2.0",
11 11
        "figlet": "^1.8.1",
12 -
        "fs-extra": "^11.2.0",
12 +
        "fs-extra": "^11.3.0",
13 13
        "ora": "^6.3.1",
14 14
        "prompts": "^2.4.2",
15 15
      },
17 17
        "@types/degit": "^2.8.6",
18 18
        "@types/figlet": "^1.7.0",
19 19
        "@types/fs-extra": "^11.0.4",
20 -
        "@types/inquirer": "^9.0.7",
21 -
        "@types/node": "^20.11.17",
20 +
        "@types/inquirer": "^9.0.8",
21 +
        "@types/node": "^20.19.0",
22 22
        "@types/prompts": "^2.4.9",
23 23
        "ts-node": "^10.9.2",
24 -
        "typescript": "^5.3.3",
24 +
        "typescript": "^5.8.3",
25 25
      },
26 26
    },
27 27
  },
src/index.ts +44 −43
6 6
import { displayBanner, createProject, DEFAULT_REPO } from "./utils";
7 7
8 8
program
9 -
	.name("create-bhvr")
10 -
	.description("Create a bhvr monorepo starter project")
11 -
	.argument("[project-directory]", "directory to create the project in")
12 -
	.option("-y, --yes", "skip confirmation prompts")
13 -
	.option("--ts, --typescript", "use TypeScript (default)")
14 -
	.option(
15 -
		"--repo <repo>",
16 -
		"specify a custom GitHub repository as source",
17 -
		DEFAULT_REPO,
18 -
	)
19 -
	.option(
20 -
		"--template <template>",
21 -
		"specify a template (default, tailwind, shadcn)",
22 -
		"default",
23 -
	)
24 -
	.option("--branch <branch>", "specify a branch to use from the repository")
25 -
	.option("--rpc", "use Hono RPC client for type-safe API communication")
9 +
  .name("create-bhvr")
10 +
  .description("Create a bhvr monorepo starter project")
11 +
  .argument("[project-directory]", "directory to create the project in")
12 +
  .option("-y, --yes", "skip confirmation prompts")
13 +
  .option("--ts, --typescript", "use TypeScript (default)")
14 +
  .option(
15 +
    "--repo <repo>",
16 +
    "specify a custom GitHub repository as source",
17 +
    DEFAULT_REPO,
18 +
  )
19 +
  .option(
20 +
    "--template <template>",
21 +
    "specify a template (default, tailwind, shadcn)",
22 +
    "default",
23 +
  )
24 +
  .option("--branch <branch>", "specify a branch to use from the repository")
25 +
  .option("--rpc", "use Hono RPC client for type-safe API communication")
26 +
	.option("--linter <linter>", "specify the linter to use (eslint or biome)")
26 27
	.action(async (projectDirectory, options) => {
27 -
		try {
28 -
			displayBanner();
29 -
			const result = await createProject(projectDirectory, options);
30 -
			if (result) {
31 -
				console.log(chalk.green.bold("🎉 Project created successfully!"));
32 -
				console.log("\nNext steps:");
28 +
    try {
29 +
      displayBanner();
30 +
      const result = await createProject(projectDirectory, options);
31 +
      if (result) {
32 +
        console.log(chalk.green.bold("🎉 Project created successfully!"));
33 +
        console.log("\nNext steps:");
33 34
34 -
				if (!result.dependenciesInstalled) {
35 -
					console.log(chalk.cyan(`  cd ${result.projectName}`));
36 -
					console.log(chalk.cyan("  bun install"));
37 -
				} else {
38 -
					console.log(chalk.cyan(`  cd ${result.projectName}`));
39 -
				}
35 +
        if (!result.dependenciesInstalled) {
36 +
          console.log(chalk.cyan(`  cd ${result.projectName}`));
37 +
          console.log(chalk.cyan("  bun install"));
38 +
        } else {
39 +
          console.log(chalk.cyan(`  cd ${result.projectName}`));
40 +
        }
40 41
41 -
				console.log(chalk.cyan("  bun run dev:client   # Start the client"));
42 -
				console.log(
43 -
					chalk.cyan(
44 -
						"  bun run dev:server   # Start the server in another terminal",
45 -
					),
46 -
				);
47 -
				console.log(chalk.cyan("  bun run dev          # Start all"));
48 -
				process.exit(0);
49 -
			}
50 -
		} catch (err) {
51 -
			console.error(chalk.red("Error creating project:"), err);
52 -
			process.exit(1);
53 -
		}
54 -
	});
42 +
        console.log(chalk.cyan("  bun run dev:client   # Start the client"));
43 +
        console.log(
44 +
          chalk.cyan(
45 +
            "  bun run dev:server   # Start the server in another terminal",
46 +
          ),
47 +
        );
48 +
        console.log(chalk.cyan("  bun run dev          # Start all"));
49 +
        process.exit(0);
50 +
      }
51 +
    } catch (err) {
52 +
      console.error(chalk.red("Error creating project:"), err);
53 +
      process.exit(1);
54 +
    }
55 +
  });
55 56
56 57
program.parse();
src/types.ts +3 −2
3 3
	description: string;
4 4
}
5 5
6 -
export interface ProjectOptions {
6 +
export type ProjectOptions = {
7 7
	yes?: boolean;
8 8
	typescript?: boolean;
9 9
	repo?: string;
10 10
	template?: string;
11 11
	branch?: string;
12 12
	rpc?: boolean;
13 -
}
13 +
	linter?: 'eslint' | 'biome';
14 +
};
14 15
15 16
export interface ProjectResult {
16 17
	projectName: string;
src/utils/helpers.ts +367 −272
5 5
import path from "node:path";
6 6
import fs from "fs-extra";
7 7
import {
8 -
	honoRpcTemplate,
9 -
	honoClientTemplate,
10 -
	shadcnTemplate,
11 -
	tailwindTemplate,
12 -
	defaultTemplate,
13 -
	TEMPLATES,
8 +
  honoRpcTemplate,
9 +
  honoClientTemplate,
10 +
  shadcnTemplate,
11 +
  tailwindTemplate,
12 +
  defaultTemplate,
13 +
  TEMPLATES,
14 14
} from "./templates";
15 15
import type { ProjectOptions, ProjectResult } from "../types";
16 16
import degit from "degit";
19 19
export const DEFAULT_REPO = "stevedylandev/bhvr";
20 20
21 21
export function displayBanner() {
22 -
	try {
23 -
		const text = figlet.textSync("bhvr", {
24 -
			font: "Big",
25 -
			horizontalLayout: "default",
26 -
			verticalLayout: "default",
27 -
			width: 80,
28 -
			whitespaceBreak: true,
29 -
		});
22 +
  try {
23 +
    const text = figlet.textSync("bhvr", {
24 +
      font: "Big",
25 +
      horizontalLayout: "default",
26 +
      verticalLayout: "default",
27 +
      width: 80,
28 +
      whitespaceBreak: true,
29 +
    });
30 30
31 -
		console.log("\n");
32 -
		console.log(chalk.yellowBright(text));
33 -
	} catch (error) {
34 -
		console.log("\n");
35 -
		console.log(chalk.yellowBright("B H V R"));
36 -
		console.log(chalk.yellow("=========="));
37 -
	}
31 +
    console.log("\n");
32 +
    console.log(chalk.yellowBright(text));
33 +
  } catch (error) {
34 +
    console.log("\n");
35 +
    console.log(chalk.yellowBright("B H V R"));
36 +
    console.log(chalk.yellow("=========="));
37 +
  }
38 38
39 -
	console.log(`\n${chalk.cyan("🦫 Lets build 🦫")}\n`);
40 -
	console.log(`${chalk.blue("https://github.com/stevedylandev/bhvr")}\n`);
39 +
  console.log(`\n${chalk.cyan("🦫 Lets build 🦫")}\n`);
40 +
  console.log(`${chalk.blue("https://github.com/stevedylandev/bhvr")}\n`);
41 41
}
42 42
43 43
export async function patchFilesForRPC(
44 -
	projectPath: string,
45 -
	templateChoice: string,
44 +
  projectPath: string,
45 +
  templateChoice: string,
46 46
): Promise<boolean> {
47 -
	const spinner = ora("Setting up RPC client...").start();
47 +
  const spinner = ora("Setting up RPC client...").start();
48 48
49 -
	try {
50 -
		// 1. Update client package.json to ensure hono client is installed
51 -
		const clientPkgPath = path.join(projectPath, "client", "package.json");
52 -
		const clientPkg = await fs.readJson(clientPkgPath);
49 +
  try {
50 +
    // 1. Update client package.json to ensure hono client is installed
51 +
    const clientPkgPath = path.join(projectPath, "client", "package.json");
52 +
    const clientPkg = await fs.readJson(clientPkgPath);
53 53
54 -
		if (!clientPkg.dependencies.hono) {
55 -
			await execa("bun", ["install", "hono"], { cwd: projectPath });
56 -
		}
54 +
    if (!clientPkg.dependencies.hono) {
55 +
      await execa("bun", ["install", "hono"], { cwd: projectPath });
56 +
    }
57 57
58 -
		await fs.writeJson(clientPkgPath, clientPkg, { spaces: 2 });
58 +
    await fs.writeJson(clientPkgPath, clientPkg, { spaces: 2 });
59 59
60 -
		// 2. Update server package.json dev script for RPC
61 -
		const serverPkgPath = path.join(projectPath, "server", "package.json");
62 -
		const serverPkg = await fs.readJson(serverPkgPath);
60 +
    // 2. Update server package.json dev script for RPC
61 +
    const serverPkgPath = path.join(projectPath, "server", "package.json");
62 +
    const serverPkg = await fs.readJson(serverPkgPath);
63 63
64 -
		// Update the dev script to include TypeScript compilation
65 -
		serverPkg.scripts.dev = "bun --watch run src/index.ts && tsc --watch";
64 +
    // Update the dev script to include TypeScript compilation
65 +
    serverPkg.scripts.dev = "bun --watch run src/index.ts && tsc --watch";
66 66
67 -
		await fs.writeJson(serverPkgPath, serverPkg, { spaces: 2 });
67 +
    await fs.writeJson(serverPkgPath, serverPkg, { spaces: 2 });
68 68
69 -
		// 3. Server modification for RPC export type (no client imports)
70 -
		const serverIndexPath = path.join(projectPath, "server", "src", "index.ts");
71 -
		await fs.writeFile(serverIndexPath, honoRpcTemplate, "utf8");
69 +
    // 3. Server modification for RPC export type (no client imports)
70 +
    const serverIndexPath = path.join(projectPath, "server", "src", "index.ts");
71 +
    await fs.writeFile(serverIndexPath, honoRpcTemplate, "utf8");
72 72
73 -
		// 4. Create separate client helper file
74 -
		const clientHelperPath = path.join(
75 -
			projectPath,
76 -
			"server",
77 -
			"src",
78 -
			"client.ts",
79 -
		);
80 -
		await fs.writeFile(clientHelperPath, honoClientTemplate, "utf8");
73 +
    // 4. Create separate client helper file
74 +
    const clientHelperPath = path.join(
75 +
      projectPath,
76 +
      "server",
77 +
      "src",
78 +
      "client.ts",
79 +
    );
80 +
    await fs.writeFile(clientHelperPath, honoClientTemplate, "utf8");
81 81
82 -
		// 5. Update App.tsx based on template selection using switch statement
83 -
		const appTsxPath = path.join(projectPath, "client", "src", "App.tsx");
82 +
    // 5. Update App.tsx based on template selection using switch statement
83 +
    const appTsxPath = path.join(projectPath, "client", "src", "App.tsx");
84 84
85 -
		// Determine template content based on the template type
86 -
		let updatedAppContent: string;
85 +
    // Determine template content based on the template type
86 +
    let updatedAppContent: string;
87 87
88 -
		// Select template based on choice
89 -
		switch (templateChoice) {
90 -
			case "shadcn":
91 -
				updatedAppContent = shadcnTemplate;
92 -
				break;
93 -
			case "tailwind":
94 -
				updatedAppContent = tailwindTemplate;
95 -
				break;
96 -
			default:
97 -
				updatedAppContent = defaultTemplate;
98 -
				break;
99 -
		}
88 +
    // Select template based on choice
89 +
    switch (templateChoice) {
90 +
      case "shadcn":
91 +
        updatedAppContent = shadcnTemplate;
92 +
        break;
93 +
      case "tailwind":
94 +
        updatedAppContent = tailwindTemplate;
95 +
        break;
96 +
      default:
97 +
        updatedAppContent = defaultTemplate;
98 +
        break;
99 +
    }
100 100
101 -
		await fs.writeFile(appTsxPath, updatedAppContent, "utf8");
102 -
		spinner.succeed("RPC client setup completed");
103 -
		return true;
104 -
	} catch (err: unknown) {
105 -
		spinner.fail("Failed to set up RPC client");
106 -
		if (err instanceof Error) {
107 -
			console.error(chalk.red("Error:"), err.message);
108 -
		} else {
109 -
			console.error(chalk.red("Error: Unknown error"));
110 -
		}
111 -
		return false;
112 -
	}
101 +
    await fs.writeFile(appTsxPath, updatedAppContent, "utf8");
102 +
    spinner.succeed("RPC client setup completed");
103 +
    return true;
104 +
  } catch (err: unknown) {
105 +
    spinner.fail("Failed to set up RPC client");
106 +
    if (err instanceof Error) {
107 +
      console.error(chalk.red("Error:"), err.message);
108 +
    } else {
109 +
      console.error(chalk.red("Error: Unknown error"));
110 +
    }
111 +
    return false;
112 +
  }
113 113
}
114 114
115 115
export async function createProject(
116 -
	projectDirectory: string,
117 -
	options: ProjectOptions,
116 +
  projectDirectory: string,
117 +
  options: ProjectOptions,
118 118
): Promise<ProjectResult | null> {
119 -
	let projectName = projectDirectory;
119 +
  let projectName = projectDirectory;
120 120
121 -
	if (!projectName && !options.yes) {
122 -
		const response = await prompts({
123 -
			type: "text",
124 -
			name: "projectName",
125 -
			message: "What is the name of your project?",
126 -
			initial: "my-bhvr-app",
127 -
		});
121 +
  if (!projectName && !options.yes) {
122 +
    const response = await prompts({
123 +
      type: "text",
124 +
      name: "projectName",
125 +
      message: "What is the name of your project?",
126 +
      initial: "my-bhvr-app",
127 +
    });
128 128
129 -
		if (!response.projectName) {
130 -
			console.log(chalk.yellow("Project creation cancelled."));
131 -
			return null;
132 -
		}
129 +
    if (!response.projectName) {
130 +
      console.log(chalk.yellow("Project creation cancelled."));
131 +
      return null;
132 +
    }
133 133
134 -
		projectName = response.projectName;
135 -
	} else if (!projectName) {
136 -
		projectName = "my-bhvr-app";
137 -
	}
134 +
    projectName = response.projectName;
135 +
  } else if (!projectName) {
136 +
    projectName = "my-bhvr-app";
137 +
  }
138 138
139 -
	let templateChoice = options.template || "default";
139 +
  let templateChoice = options.template || "default";
140 140
141 -
	if (!options.yes && !options.branch) {
142 -
		const templateChoices = Object.keys(TEMPLATES).map((key) => ({
143 -
			title: `${key} (${TEMPLATES[key]?.description})`,
144 -
			value: key,
145 -
		}));
141 +
  if (!options.yes && !options.branch) {
142 +
    const templateChoices = Object.keys(TEMPLATES).map((key) => ({
143 +
      title: `${key} (${TEMPLATES[key]?.description})`,
144 +
      value: key,
145 +
    }));
146 146
147 -
		const templateResponse = await prompts({
148 -
			type: "select",
149 -
			name: "template",
150 -
			message: "Select a template:",
151 -
			choices: templateChoices,
152 -
			initial: 0,
153 -
		});
147 +
    const templateResponse = await prompts({
148 +
      type: "select",
149 +
      name: "template",
150 +
      message: "Select a template:",
151 +
      choices: templateChoices,
152 +
      initial: 0,
153 +
    });
154 154
155 -
		if (templateResponse.template === undefined) {
156 -
			console.log(chalk.yellow("Project creation cancelled."));
157 -
			return null;
158 -
		}
155 +
    if (templateResponse.template === undefined) {
156 +
      console.log(chalk.yellow("Project creation cancelled."));
157 +
      return null;
158 +
    }
159 159
160 -
		templateChoice = templateResponse.template;
161 -
	}
160 +
    templateChoice = templateResponse.template;
161 +
  }
162 162
163 -
	const projectPath = path.resolve(process.cwd(), projectName);
163 +
  const projectPath = path.resolve(process.cwd(), projectName);
164 164
165 -
	if (fs.existsSync(projectPath)) {
166 -
		const files = fs.readdirSync(projectPath);
165 +
  if (fs.existsSync(projectPath)) {
166 +
    const files = fs.readdirSync(projectPath);
167 167
168 -
		if (files.length > 0 && !options.yes) {
169 -
			const { overwrite } = await prompts({
170 -
				type: "confirm",
171 -
				name: "overwrite",
172 -
				message: `The directory ${projectName} already exists and is not empty. Do you want to overwrite it?`,
173 -
				initial: false,
174 -
			});
168 +
    if (files.length > 0 && !options.yes) {
169 +
      const { overwrite } = await prompts({
170 +
        type: "confirm",
171 +
        name: "overwrite",
172 +
        message: `The directory ${projectName} already exists and is not empty. Do you want to overwrite it?`,
173 +
        initial: false,
174 +
      });
175 175
176 -
			if (!overwrite) {
177 -
				console.log(chalk.yellow("Project creation cancelled."));
178 -
				return null;
179 -
			}
176 +
      if (!overwrite) {
177 +
        console.log(chalk.yellow("Project creation cancelled."));
178 +
        return null;
179 +
      }
180 +
181 +
      await fs.emptyDir(projectPath);
182 +
    }
183 +
  }
184 +
185 +
  fs.ensureDirSync(projectPath);
186 +
187 +
  const repoPath = options.repo || DEFAULT_REPO;
188 +
  const templateConfig =
189 +
    TEMPLATES[templateChoice as keyof typeof TEMPLATES] || TEMPLATES.default;
190 +
  const branch = options.branch || (templateConfig?.branch ?? "main");
191 +
  const repoUrl = `${repoPath}#${branch}`;
192 +
  const spinner = ora("Downloading template...").start();
193 +
194 +
  try {
195 +
    const emitter = degit(repoUrl, {
196 +
      cache: false,
197 +
      force: true,
198 +
      verbose: false,
199 +
    });
200 +
201 +
    await emitter.clone(projectPath);
202 +
    spinner.succeed(
203 +
      `Template downloaded successfully (${templateChoice} template)`,
204 +
    );
205 +
206 +
    const pkgJsonPath = path.join(projectPath, "package.json");
207 +
    if (fs.existsSync(pkgJsonPath)) {
208 +
      const pkgJson = await fs.readJson(pkgJsonPath);
209 +
      pkgJson.name = projectName;
210 +
      await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
211 +
    }
212 +
213 +
    const gitDir = path.join(projectPath, ".git");
214 +
    if (fs.existsSync(gitDir)) {
215 +
      await fs.remove(gitDir);
216 +
      console.log(chalk.blue("Removed .git directory"));
217 +
    }
218 +
219 +
    let useRpc = options.rpc;
220 +
221 +
    if (!options.yes && !options.rpc) {
222 +
      const rpcResponse = await prompts({
223 +
        type: "confirm",
224 +
        name: "useRpc",
225 +
        message: "Use Hono RPC client for type-safe API communication?",
226 +
        initial: false,
227 +
      });
228 +
229 +
      if (rpcResponse.useRpc === undefined) {
230 +
        console.log(chalk.yellow("Project creation cancelled."));
231 +
        return null;
232 +
      }
233 +
234 +
      useRpc = rpcResponse.useRpc;
235 +
    }
236 +
237 +
    if (useRpc) {
238 +
      await patchFilesForRPC(projectPath, templateChoice);
239 +
    }
240 +
241 +
    let linter = options.linter;
180 242
181 -
			await fs.emptyDir(projectPath);
182 -
		}
183 -
	}
243 +
    if (!options.yes && !options.linter) {
244 +
      const linterResponse = await prompts({
245 +
        type: "select",
246 +
        name: "linter",
247 +
        message: "Select a linter:",
248 +
        choices: [
249 +
          { title: "ESLint (default)", value: "eslint" },
250 +
          { title: "Biome", value: "biome" },
251 +
        ],
252 +
        initial: 0,
253 +
      });
184 254
185 -
	fs.ensureDirSync(projectPath);
255 +
      if (linterResponse.linter === undefined) {
256 +
        console.log(chalk.yellow("Project creation cancelled."));
257 +
        return null;
258 +
      }
186 259
187 -
	const repoPath = options.repo || DEFAULT_REPO;
188 -
	const templateConfig =
189 -
		TEMPLATES[templateChoice as keyof typeof TEMPLATES] || TEMPLATES.default;
190 -
	const branch = options.branch || (templateConfig?.branch ?? "main");
191 -
	const repoUrl = `${repoPath}#${branch}`;
192 -
	const spinner = ora("Downloading template...").start();
260 +
      linter = linterResponse.linter;
261 +
    }
193 262
194 -
	try {
195 -
		const emitter = degit(repoUrl, {
196 -
			cache: false,
197 -
			force: true,
198 -
			verbose: false,
199 -
		});
263 +
    if (linter === "biome") {
264 +
      await setupBiome(projectPath);
265 +
    }
200 266
201 -
		await emitter.clone(projectPath);
202 -
		spinner.succeed(
203 -
			`Template downloaded successfully (${templateChoice} template)`,
204 -
		);
267 +
    let gitInitialized = false;
205 268
206 -
		const pkgJsonPath = path.join(projectPath, "package.json");
207 -
		if (fs.existsSync(pkgJsonPath)) {
208 -
			const pkgJson = await fs.readJson(pkgJsonPath);
209 -
			pkgJson.name = projectName;
210 -
			await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
211 -
		}
269 +
    if (!options.yes) {
270 +
      const gitResponse = await prompts({
271 +
        type: "confirm",
272 +
        name: "initGit",
273 +
        message: "Initialize a git repository?",
274 +
        initial: true,
275 +
      });
212 276
213 -
		const gitDir = path.join(projectPath, ".git");
214 -
		if (fs.existsSync(gitDir)) {
215 -
			await fs.remove(gitDir);
216 -
			console.log(chalk.blue("Removed .git directory"));
217 -
		}
277 +
      if (gitResponse.initGit) {
278 +
        try {
279 +
          spinner.start("Initializing git repository...");
280 +
          await execa("git", ["init"], { cwd: projectPath });
281 +
          spinner.succeed("Git repository initialized");
282 +
          gitInitialized = true;
283 +
        } catch (err: unknown) {
284 +
          spinner.fail(
285 +
            "Failed to initialize git repository. Is git installed?",
286 +
          );
287 +
          if (err instanceof Error) {
288 +
            console.error(chalk.red("Git error:"), err.message);
289 +
          } else {
290 +
            console.error(chalk.red("Git error: Unknown error"));
291 +
          }
292 +
        }
293 +
      }
294 +
    } else {
295 +
      try {
296 +
        spinner.start("Initializing git repository...");
297 +
        await execa("git", ["init"], { cwd: projectPath });
298 +
        spinner.succeed("Git repository initialized");
299 +
        gitInitialized = true;
300 +
      } catch (err) {
301 +
        spinner.fail("Failed to initialize git repository. Is git installed?");
302 +
      }
303 +
    }
218 304
219 -
		let useRpc = options.rpc;
305 +
    let dependenciesInstalled = false;
220 306
221 -
		if (!options.yes && !options.rpc) {
222 -
			const rpcResponse = await prompts({
223 -
				type: "confirm",
224 -
				name: "useRpc",
225 -
				message: "Use Hono RPC client for type-safe API communication?",
226 -
				initial: false,
227 -
			});
307 +
    if (!options.yes) {
308 +
      const depsResponse = await prompts({
309 +
        type: "confirm",
310 +
        name: "installDeps",
311 +
        message: "Install dependencies?",
312 +
        initial: true,
313 +
      });
228 314
229 -
			if (rpcResponse.useRpc === undefined) {
230 -
				console.log(chalk.yellow("Project creation cancelled."));
231 -
				return null;
232 -
			}
315 +
      if (depsResponse.installDeps) {
316 +
        spinner.start("Installing dependencies...");
317 +
        try {
318 +
          await execa("bun", ["install"], { cwd: projectPath });
319 +
          spinner.succeed("Dependencies installed with bun");
320 +
          dependenciesInstalled = true;
321 +
        } catch (bunErr) {
322 +
          try {
323 +
            spinner.text = "Installing dependencies with npm...";
324 +
            await execa("npm", ["install"], { cwd: projectPath });
325 +
            spinner.succeed("Dependencies installed with npm");
326 +
            dependenciesInstalled = true;
327 +
          } catch (npmErr) {
328 +
            spinner.fail("Failed to install dependencies.");
329 +
            console.log(
330 +
              chalk.yellow(
331 +
                "You can install them manually after navigating to the project directory.",
332 +
              ),
333 +
            );
334 +
          }
335 +
        }
336 +
      }
337 +
    } else {
338 +
      spinner.start("Installing dependencies...");
339 +
      try {
340 +
        await execa("bun", ["install"], { cwd: projectPath });
341 +
        spinner.succeed("Dependencies installed with bun");
342 +
        dependenciesInstalled = true;
343 +
      } catch (bunErr) {
344 +
        try {
345 +
          spinner.text = "Installing dependencies with npm...";
346 +
          await execa("npm", ["install"], { cwd: projectPath });
347 +
          spinner.succeed("Dependencies installed with npm");
348 +
          dependenciesInstalled = true;
349 +
        } catch (npmErr) {
350 +
          spinner.fail(
351 +
            "Failed to install dependencies. You can install them manually later.",
352 +
          );
353 +
        }
354 +
      }
355 +
    }
233 356
234 -
			useRpc = rpcResponse.useRpc;
235 -
		}
357 +
    return {
358 +
      projectName,
359 +
      gitInitialized,
360 +
      dependenciesInstalled,
361 +
      template: templateChoice,
362 +
    };
363 +
  } catch (err) {
364 +
    spinner.fail("Failed to download template");
365 +
    throw err;
366 +
  }
367 +
}
236 368
237 -
		if (useRpc) {
238 -
			await patchFilesForRPC(projectPath, templateChoice);
239 -
		}
369 +
export async function setupBiome(projectPath: string): Promise<void> {
370 +
  const spinner = ora("Setting up Biome...").start();
371 +
  try {
372 +
    const clientPath = path.join(projectPath, "client");
373 +
    const clientPkgJsonPath = path.join(clientPath, "package.json");
374 +
    const eslintConfigPath = path.join(clientPath, "eslint.config.js");
240 375
241 -
		let gitInitialized = false;
376 +
    // Remove ESLint config file
377 +
    if (fs.existsSync(eslintConfigPath)) {
378 +
      await fs.remove(eslintConfigPath);
379 +
    }
242 380
243 -
		if (!options.yes) {
244 -
			const gitResponse = await prompts({
245 -
				type: "confirm",
246 -
				name: "initGit",
247 -
				message: "Initialize a git repository?",
248 -
				initial: true,
249 -
			});
381 +
    // Read client package.json and remove ESLint dependencies
382 +
    const clientPkgJson = await fs.readJson(clientPkgJsonPath);
383 +
    const devDependencies = clientPkgJson.devDependencies || {};
384 +
    const eslintDeps = Object.keys(devDependencies).filter(
385 +
      (dep) => dep.startsWith("eslint") || dep.startsWith("@eslint"),
386 +
    );
250 387
251 -
			if (gitResponse.initGit) {
252 -
				try {
253 -
					spinner.start("Initializing git repository...");
254 -
					await execa("git", ["init"], { cwd: projectPath });
255 -
					spinner.succeed("Git repository initialized");
256 -
					gitInitialized = true;
257 -
				} catch (err: unknown) {
258 -
					spinner.fail(
259 -
						"Failed to initialize git repository. Is git installed?",
260 -
					);
261 -
					if (err instanceof Error) {
262 -
						console.error(chalk.red("Git error:"), err.message);
263 -
					} else {
264 -
						console.error(chalk.red("Git error: Unknown error"));
265 -
					}
266 -
				}
267 -
			}
268 -
		} else {
269 -
			try {
270 -
				spinner.start("Initializing git repository...");
271 -
				await execa("git", ["init"], { cwd: projectPath });
272 -
				spinner.succeed("Git repository initialized");
273 -
				gitInitialized = true;
274 -
			} catch (err) {
275 -
				spinner.fail("Failed to initialize git repository. Is git installed?");
276 -
			}
277 -
		}
388 +
    if (eslintDeps.length > 0) {
389 +
      spinner.text = "Replacing ESLint dependencies...";
390 +
      await execa("bun", ["remove", ...eslintDeps], { cwd: clientPath });
391 +
    }
278 392
279 -
		let dependenciesInstalled = false;
393 +
    // Install Biome
394 +
    spinner.text = "Installing Biome...";
395 +
    await execa("bun", ["add", "-D", "@biomejs/biome"], { cwd: clientPath });
280 396
281 -
		if (!options.yes) {
282 -
			const depsResponse = await prompts({
283 -
				type: "confirm",
284 -
				name: "installDeps",
285 -
				message: "Install dependencies?",
286 -
				initial: true,
287 -
			});
397 +
    // Create biome.json in client workspace
398 +
    spinner.text = "Creating biome.json...";
399 +
    const biomeConfig = {
400 +
      $schema: "https://biomejs.dev/schemas/1.7.3/schema.json",
401 +
      vcs: {
402 +
        enabled: true,
403 +
        clientKind: "git",
404 +
        useIgnoreFile: true,
405 +
      },
406 +
      files: { ignoreUnknown: false, ignore: [] },
407 +
      formatter: { enabled: true },
408 +
      organizeImports: { enabled: true },
409 +
      linter: {
410 +
        enabled: true,
411 +
        rules: {
412 +
          recommended: true,
413 +
        },
414 +
      },
415 +
    };
416 +
    const biomeConfigPath = path.join(clientPath, "biome.json");
417 +
    await fs.writeJson(biomeConfigPath, biomeConfig, { spaces: 2 });
288 418
289 -
			if (depsResponse.installDeps) {
290 -
				spinner.start("Installing dependencies...");
291 -
				try {
292 -
					await execa("bun", ["install"], { cwd: projectPath });
293 -
					spinner.succeed("Dependencies installed with bun");
294 -
					dependenciesInstalled = true;
295 -
				} catch (bunErr) {
296 -
					try {
297 -
						spinner.text = "Installing dependencies with npm...";
298 -
						await execa("npm", ["install"], { cwd: projectPath });
299 -
						spinner.succeed("Dependencies installed with npm");
300 -
						dependenciesInstalled = true;
301 -
					} catch (npmErr) {
302 -
						spinner.fail("Failed to install dependencies.");
303 -
						console.log(
304 -
							chalk.yellow(
305 -
								"You can install them manually after navigating to the project directory.",
306 -
							),
307 -
						);
308 -
					}
309 -
				}
310 -
			}
311 -
		} else {
312 -
			spinner.start("Installing dependencies...");
313 -
			try {
314 -
				await execa("bun", ["install"], { cwd: projectPath });
315 -
				spinner.succeed("Dependencies installed with bun");
316 -
				dependenciesInstalled = true;
317 -
			} catch (bunErr) {
318 -
				try {
319 -
					spinner.text = "Installing dependencies with npm...";
320 -
					await execa("npm", ["install"], { cwd: projectPath });
321 -
					spinner.succeed("Dependencies installed with npm");
322 -
					dependenciesInstalled = true;
323 -
				} catch (npmErr) {
324 -
					spinner.fail(
325 -
						"Failed to install dependencies. You can install them manually later.",
326 -
					);
327 -
				}
328 -
			}
329 -
		}
419 +
    // Update client package.json scripts
420 +
    spinner.text = "Updating scripts in client/package.json...";
421 +
    const newClientPkgJson = await fs.readJson(clientPkgJsonPath);
422 +
    delete newClientPkgJson.scripts.lint; // Remove old lint script
423 +
    newClientPkgJson.scripts.format = "biome format . --write";
424 +
    newClientPkgJson.scripts.lint = "biome lint .";
425 +
    await fs.writeJson(clientPkgJsonPath, newClientPkgJson, { spaces: 2 });
330 426
331 -
		return {
332 -
			projectName,
333 -
			gitInitialized,
334 -
			dependenciesInstalled,
335 -
			template: templateChoice,
336 -
		};
337 -
	} catch (err) {
338 -
		spinner.fail("Failed to download template");
339 -
		throw err;
340 -
	}
427 +
    spinner.succeed("Biome setup complete.");
428 +
  } catch (error) {
429 +
    spinner.fail("Biome setup failed.");
430 +
    if (error instanceof Error) {
431 +
      console.error(chalk.red("\nError:"), error.message);
432 +
    } else {
433 +
      console.error(chalk.red("\nError: Unknown error during Biome setup."));
434 +
    }
435 +
  }
341 436
}