Replaced chalk with picocolors, replaced prompts with consola. Organized folder structure. fd558650
Pedro Santana · 2025-07-11 18:03 16 file(s) · +577 −539
bun.lock +5 −5
4 4
    "": {
5 5
      "name": "create-bhvr",
6 6
      "dependencies": {
7 -
        "chalk": "^5.4.1",
8 7
        "commander": "^11.1.0",
8 +
        "consola": "^3.4.2",
9 9
        "degit": "^2.8.4",
10 10
        "execa": "^7.2.0",
11 11
        "figlet": "^1.8.1",
12 12
        "fs-extra": "^11.3.0",
13 13
        "ora": "^6.3.1",
14 -
        "prompts": "^2.4.2",
14 +
        "picocolors": "^1.1.1",
15 15
      },
16 16
      "devDependencies": {
17 17
        "@types/degit": "^2.8.6",
82 82
83 83
    "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
84 84
85 +
    "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
86 +
85 87
    "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="],
86 88
87 89
    "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
136 138
137 139
    "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
138 140
139 -
    "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
141 +
    "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
140 142
141 143
    "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
142 144
151 153
    "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
152 154
153 155
    "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
154 -
155 -
    "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
156 156
157 157
    "stdin-discarder": ["stdin-discarder@0.1.0", "", { "dependencies": { "bl": "^5.0.0" } }, "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ=="],
158 158
package.json +51 −51
1 1
{
2 -
	"name": "create-bhvr",
3 -
	"version": "0.3.12",
4 -
	"description": "Create a new bhvr project",
5 -
	"type": "module",
6 -
	"main": "./dist/index.js",
7 -
	"types": "./dist/index.d.ts",
8 -
	"bin": {
9 -
		"create-bhvr": "./dist/index.js"
10 -
	},
11 -
	"files": [
12 -
		"dist"
13 -
	],
14 -
	"scripts": {
15 -
		"build": "bun build src/index.ts --outdir dist --target node",
16 -
		"start": "bun ./dist/index.js"
17 -
	},
18 -
	"keywords": [
19 -
		"bun",
20 -
		"vite",
21 -
		"react",
22 -
		"hono",
23 -
		"monorepo",
24 -
		"starter",
25 -
		"template",
26 -
		"create"
27 -
	],
28 -
	"author": "Steve Simkins",
29 -
	"license": "MIT",
30 -
	"devDependencies": {
31 -
		"@types/degit": "^2.8.6",
32 -
		"@types/figlet": "^1.7.0",
33 -
		"@types/fs-extra": "^11.0.4",
34 -
		"@types/inquirer": "^9.0.8",
35 -
		"@types/node": "^20.19.0",
36 -
		"@types/prompts": "^2.4.9",
37 -
		"ts-node": "^10.9.2",
38 -
		"typescript": "^5.8.3"
39 -
	},
40 -
	"dependencies": {
41 -
		"chalk": "^5.4.1",
42 -
		"commander": "^11.1.0",
43 -
		"degit": "^2.8.4",
44 -
		"execa": "^7.2.0",
45 -
		"figlet": "^1.8.1",
46 -
		"fs-extra": "^11.3.0",
47 -
		"ora": "^6.3.1",
48 -
		"prompts": "^2.4.2"
49 -
	},
50 -
	"engines": {
51 -
		"node": ">=18"
52 -
	}
2 +
  "name": "create-bhvr",
3 +
  "version": "0.3.12",
4 +
  "description": "Create a new bhvr project",
5 +
  "type": "module",
6 +
  "main": "./dist/index.js",
7 +
  "types": "./dist/index.d.ts",
8 +
  "bin": {
9 +
    "create-bhvr": "./dist/index.js"
10 +
  },
11 +
  "files": [
12 +
    "dist"
13 +
  ],
14 +
  "scripts": {
15 +
    "build": "bun build src/index.ts --outdir dist --target node",
16 +
    "start": "bun ./dist/index.js"
17 +
  },
18 +
  "keywords": [
19 +
    "bun",
20 +
    "vite",
21 +
    "react",
22 +
    "hono",
23 +
    "monorepo",
24 +
    "starter",
25 +
    "template",
26 +
    "create"
27 +
  ],
28 +
  "author": "Steve Simkins",
29 +
  "license": "MIT",
30 +
  "devDependencies": {
31 +
    "@types/degit": "^2.8.6",
32 +
    "@types/figlet": "^1.7.0",
33 +
    "@types/fs-extra": "^11.0.4",
34 +
    "@types/inquirer": "^9.0.8",
35 +
    "@types/node": "^20.19.0",
36 +
    "@types/prompts": "^2.4.9",
37 +
    "ts-node": "^10.9.2",
38 +
    "typescript": "^5.8.3"
39 +
  },
40 +
  "dependencies": {
41 +
    "commander": "^11.1.0",
42 +
    "consola": "^3.4.2",
43 +
    "degit": "^2.8.4",
44 +
    "execa": "^7.2.0",
45 +
    "figlet": "^1.8.1",
46 +
    "fs-extra": "^11.3.0",
47 +
    "ora": "^6.3.1",
48 +
    "picocolors": "^1.1.1"
49 +
  },
50 +
  "engines": {
51 +
    "node": ">=18"
52 +
  }
53 53
}
src/commands/create.ts (added) +1 −0
1 +
import { displayBanner } from "@/lib/display-banner";import { createProject } from "../lib/create-project";import pc from "picocolors";export const create = async (projectDirectory: string, options: any) => {  try {    displayBanner();    const result = await createProject(projectDirectory, options);    if (result) {      console.log(pc.green(pc.bold("🎉 Project created successfully!")));      console.log("\nNext steps:");      if (!result.dependenciesInstalled) {        console.log(pc.cyan(`  cd ${result.projectName}`));        console.log(pc.cyan("  bun install"));      } else {        console.log(pc.cyan(`  cd ${result.projectName}`));      }      console.log(pc.cyan("  bun run dev:client   # Start the client"));      console.log(        pc.cyan(          "  bun run dev:server   # Start the server in another terminal",        ),      );      console.log(pc.cyan("  bun run dev          # Start all"));      process.exit(0);    }  } catch (err) {    console.error(pc.red("Error creating project:"), err);    process.exit(1);  }};
src/index.ts +6 −33
1 1
#!/usr/bin/env node
2 +
2 3
// @ts-ignore: Shebang line
3 4
4 -
import { program } from "commander";
5 -
import chalk from "chalk";
6 -
import { displayBanner, createProject, DEFAULT_REPO } from "./utils";
5 +
import { create } from "@/commands/create";
6 +
import { program } from "@/program";
7 +
import { DEFAULT_REPO } from "./utils";
7 8
8 9
program
9 10
  .name("create-bhvr")
23 24
  )
24 25
  .option("--branch <branch>", "specify a branch to use from the repository")
25 26
  .option("--rpc", "use Hono RPC client for type-safe API communication")
26 -
	.option("--linter <linter>", "specify the linter to use (eslint or biome)")
27 -
	.action(async (projectDirectory, options) => {
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:");
34 -
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 -
        }
41 -
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 -
  });
27 +
  .option("--linter <linter>", "specify the linter to use (eslint or biome)")
28 +
  .action(create);
56 29
57 30
program.parse();
src/lib/create-project.ts (added) +285 −0
1 +
import path from "node:path";
2 +
import { consola } from "consola";
3 +
import degit from "degit";
4 +
import { execa } from "execa";
5 +
import fs from "fs-extra";
6 +
import ora from "ora";
7 +
import pc from "picocolors";
8 +
import type { ProjectOptions, ProjectResult } from "@/types";
9 +
import { DEFAULT_REPO } from "@/utils/constants";
10 +
import { TEMPLATES } from "@/utils/templates";
11 +
import { patchFilesForRPC } from "./patch-files-rpc";
12 +
import { setupBiome } from "./setup-biome";
13 +
import { tryCatch } from "@/utils/try-catch";
14 +
15 +
export async function createProject(
16 +
  projectDirectory: string,
17 +
  options: ProjectOptions,
18 +
): Promise<ProjectResult | null> {
19 +
  let projectName = projectDirectory;
20 +
21 +
  if (!projectName && !options.yes) {
22 +
    const { data, error } = await tryCatch(
23 +
      consola.prompt(pc.yellow("What is the name of your project?"), {
24 +
        type: "text",
25 +
        default: "my-bhvr-app",
26 +
        placeholder: "my-bhvr-app",
27 +
        cancel: "reject",
28 +
      }),
29 +
    );
30 +
31 +
    if (!data || error) {
32 +
      consola.error(pc.red("Project creation cancelled."));
33 +
      return null;
34 +
    }
35 +
36 +
    projectName = data;
37 +
  }
38 +
39 +
  let templateChoice = options.template || "default";
40 +
41 +
  if (!options.yes && !options.branch) {
42 +
    const templateChoices = Object.keys(TEMPLATES).map((key) => ({
43 +
      label: `${key} (${TEMPLATES[key]?.description})`,
44 +
      value: key,
45 +
    }));
46 +
47 +
    const { data, error } = await tryCatch(
48 +
      consola.prompt(pc.yellow("Select a template:"), {
49 +
        type: "select",
50 +
        options: templateChoices,
51 +
        initial: "default",
52 +
        cancel: "reject",
53 +
      }),
54 +
    );
55 +
56 +
    if (!data || error) {
57 +
      consola.error("Project creation cancelled.");
58 +
      return null;
59 +
    }
60 +
61 +
    templateChoice = data;
62 +
  }
63 +
64 +
  const projectPath = path.resolve(process.cwd(), projectName);
65 +
66 +
  if (fs.existsSync(projectPath)) {
67 +
    const files = fs.readdirSync(projectPath);
68 +
69 +
    if (files.length > 0 && !options.yes) {
70 +
      const { data: overwrite, error } = await tryCatch(
71 +
        consola.prompt(
72 +
          `The directory ${projectName} already exists and is not empty. Do you want to overwrite it?`,
73 +
          {
74 +
            type: "confirm",
75 +
            initial: false,
76 +
          },
77 +
        ),
78 +
      );
79 +
80 +
      if (!overwrite || error) {
81 +
        consola.error("Project creation cancelled.");
82 +
        return null;
83 +
      }
84 +
85 +
      await fs.emptyDir(projectPath);
86 +
    }
87 +
  }
88 +
89 +
  fs.ensureDirSync(projectPath);
90 +
91 +
  const repoPath = options.repo || DEFAULT_REPO;
92 +
  const templateConfig =
93 +
    TEMPLATES[templateChoice as keyof typeof TEMPLATES] || TEMPLATES.default;
94 +
  const branch = options.branch || (templateConfig?.branch ?? "main");
95 +
  const repoUrl = `${repoPath}#${branch}`;
96 +
  const spinner = ora("Downloading template...").start();
97 +
98 +
  try {
99 +
    const emitter = degit(repoUrl, {
100 +
      cache: false,
101 +
      force: true,
102 +
      verbose: false,
103 +
    });
104 +
105 +
    await emitter.clone(projectPath);
106 +
    spinner.succeed(
107 +
      `Template downloaded successfully (${templateChoice} template)`,
108 +
    );
109 +
110 +
    const pkgJsonPath = path.join(projectPath, "package.json");
111 +
    if (fs.existsSync(pkgJsonPath)) {
112 +
      const pkgJson = await fs.readJson(pkgJsonPath);
113 +
      pkgJson.name = projectName;
114 +
      await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
115 +
    }
116 +
117 +
    const gitDir = path.join(projectPath, ".git");
118 +
    if (fs.existsSync(gitDir)) {
119 +
      await fs.remove(gitDir);
120 +
      console.log(pc.blue("Removed .git directory"));
121 +
    }
122 +
123 +
    let useRpc = options.rpc;
124 +
125 +
    if (!options.yes && !options.rpc) {
126 +
      const { data: rpcResponse, error } = await tryCatch(
127 +
        consola.prompt("Use Hono RPC client for type-safe API communication?", {
128 +
          type: "confirm",
129 +
          initial: false,
130 +
        }),
131 +
      );
132 +
133 +
      if (!rpcResponse || error) {
134 +
        consola.error("Project creation cancelled.");
135 +
        return null;
136 +
      }
137 +
138 +
      useRpc = rpcResponse;
139 +
    }
140 +
141 +
    if (useRpc) {
142 +
      await patchFilesForRPC(projectPath, templateChoice);
143 +
    }
144 +
145 +
    let linter = options.linter;
146 +
147 +
    if (!options.yes && !options.linter) {
148 +
      const { data: linterResponse, error } = await tryCatch(
149 +
        consola.prompt("Select a linter:", {
150 +
          type: "select",
151 +
          options: [
152 +
            { label: "ESLint (default)", value: "eslint" },
153 +
            { label: "Biome", value: "biome" },
154 +
          ],
155 +
          initial: "eslint",
156 +
          cancel: "reject",
157 +
        }),
158 +
      );
159 +
160 +
      if (!linterResponse || error) {
161 +
        console.log(pc.yellow("Project creation cancelled."));
162 +
        return null;
163 +
      }
164 +
165 +
      linter = linterResponse as "eslint" | "biome";
166 +
    }
167 +
168 +
    if (linter === "biome") {
169 +
      await setupBiome(projectPath);
170 +
    }
171 +
172 +
    let gitInitialized = false;
173 +
174 +
    if (!options.yes) {
175 +
      const { data: gitResponse, error } = await tryCatch(
176 +
        consola.prompt("Initialize a git repository?", {
177 +
          type: "confirm",
178 +
179 +
          initial: true,
180 +
          cancel: "reject",
181 +
        }),
182 +
      );
183 +
184 +
      if (error) {
185 +
        console.log(pc.yellow("Project creation cancelled."));
186 +
        return null;
187 +
      }
188 +
189 +
      if (gitResponse) {
190 +
        try {
191 +
          spinner.start("Initializing git repository...");
192 +
          await execa("git", ["init"], { cwd: projectPath });
193 +
          spinner.succeed("Git repository initialized");
194 +
          gitInitialized = true;
195 +
        } catch (err: unknown) {
196 +
          spinner.fail(
197 +
            "Failed to initialize git repository. Is git installed?",
198 +
          );
199 +
          if (err instanceof Error) {
200 +
            consola.error(pc.red("Git error:"), err.message);
201 +
          } else {
202 +
            consola.error(pc.red("Git error: Unknown error"));
203 +
          }
204 +
        }
205 +
      }
206 +
    } else {
207 +
      try {
208 +
        spinner.start("Initializing git repository...");
209 +
        await execa("git", ["init"], { cwd: projectPath });
210 +
        spinner.succeed("Git repository initialized");
211 +
        gitInitialized = true;
212 +
      } catch (_err) {
213 +
        spinner.fail("Failed to initialize git repository. Is git installed?");
214 +
      }
215 +
    }
216 +
217 +
    let dependenciesInstalled = false;
218 +
219 +
    if (!options.yes) {
220 +
      const { data: depsResponse, error } = await tryCatch(
221 +
        consola.prompt("Install dependencies?", {
222 +
          type: "confirm",
223 +
          initial: true,
224 +
          cancel: "reject",
225 +
        }),
226 +
      );
227 +
228 +
      if (error) {
229 +
        console.log(pc.yellow("Project creation cancelled."));
230 +
        return null;
231 +
      }
232 +
233 +
      if (depsResponse) {
234 +
        spinner.start("Installing dependencies...");
235 +
        try {
236 +
          await execa("bun", ["install"], { cwd: projectPath });
237 +
          spinner.succeed("Dependencies installed with bun");
238 +
          dependenciesInstalled = true;
239 +
        } catch (_bunErr) {
240 +
          try {
241 +
            spinner.text = "Installing dependencies with npm...";
242 +
            await execa("npm", ["install"], { cwd: projectPath });
243 +
            spinner.succeed("Dependencies installed with npm");
244 +
            dependenciesInstalled = true;
245 +
          } catch (_npmErr) {
246 +
            spinner.fail("Failed to install dependencies.");
247 +
            console.log(
248 +
              pc.yellow(
249 +
                "You can install them manually after navigating to the project directory.",
250 +
              ),
251 +
            );
252 +
          }
253 +
        }
254 +
      }
255 +
    } else {
256 +
      spinner.start("Installing dependencies...");
257 +
      try {
258 +
        await execa("bun", ["install"], { cwd: projectPath });
259 +
        spinner.succeed("Dependencies installed with bun");
260 +
        dependenciesInstalled = true;
261 +
      } catch (_bunErr) {
262 +
        try {
263 +
          spinner.text = "Installing dependencies with npm...";
264 +
          await execa("npm", ["install"], { cwd: projectPath });
265 +
          spinner.succeed("Dependencies installed with npm");
266 +
          dependenciesInstalled = true;
267 +
        } catch (_npmErr) {
268 +
          spinner.fail(
269 +
            "Failed to install dependencies. You can install them manually later.",
270 +
          );
271 +
        }
272 +
      }
273 +
    }
274 +
275 +
    return {
276 +
      projectName,
277 +
      gitInitialized,
278 +
      dependenciesInstalled,
279 +
      template: templateChoice,
280 +
    };
281 +
  } catch (err) {
282 +
    spinner.fail("Failed to download template");
283 +
    throw err;
284 +
  }
285 +
}
src/lib/display-banner.ts (added) +19 −0
1 +
import { consola } from "consola";
2 +
import figlet from "figlet";
3 +
import pc from "picocolors";
4 +
5 +
export function displayBanner() {
6 +
  const text = figlet.textSync("bhvr", {
7 +
    font: "Big",
8 +
    horizontalLayout: "default",
9 +
    verticalLayout: "default",
10 +
    width: 80,
11 +
    whitespaceBreak: true,
12 +
  });
13 +
14 +
  console.log("\n");
15 +
  console.log(pc.yellowBright(text));
16 +
17 +
  consola.info(`${pc.cyan("🦫 Lets build 🦫")}`);
18 +
  consola.info(`${pc.blue("https://github.com/stevedylandev/bhvr")}\n`);
19 +
}
src/lib/index.ts (added) +4 −0
1 +
export * from "./create-project";
2 +
export * from "./display-banner";
3 +
export * from "./patch-files-rpc";
4 +
export * from "./setup-biome";
src/lib/patch-files-rpc.ts (added) +85 −0
1 +
import path from "node:path";
2 +
import { consola } from "consola";
3 +
import { execa } from "execa";
4 +
import fs from "fs-extra";
5 +
import ora from "ora";
6 +
import pc from "picocolors";
7 +
import {
8 +
  defaultTemplate,
9 +
  honoClientTemplate,
10 +
  honoRpcTemplate,
11 +
  shadcnTemplate,
12 +
  tailwindTemplate,
13 +
} from "@/utils/templates";
14 +
15 +
export async function patchFilesForRPC(
16 +
  projectPath: string,
17 +
  templateChoice: string,
18 +
): Promise<boolean> {
19 +
  const spinner = ora("Setting up RPC client...").start();
20 +
21 +
  try {
22 +
    // 1. Update client package.json to ensure hono client is installed
23 +
    const clientPkgPath = path.join(projectPath, "client", "package.json");
24 +
    const clientPkg = await fs.readJson(clientPkgPath);
25 +
26 +
    if (!clientPkg.dependencies.hono) {
27 +
      await execa("bun", ["install", "hono"], { cwd: projectPath });
28 +
    }
29 +
30 +
    await fs.writeJson(clientPkgPath, clientPkg, { spaces: 2 });
31 +
32 +
    // 2. Update server package.json dev script for RPC
33 +
    const serverPkgPath = path.join(projectPath, "server", "package.json");
34 +
    const serverPkg = await fs.readJson(serverPkgPath);
35 +
36 +
    // Update the dev script to include TypeScript compilation
37 +
    serverPkg.scripts.dev = "bun --watch run src/index.ts && tsc --watch";
38 +
39 +
    await fs.writeJson(serverPkgPath, serverPkg, { spaces: 2 });
40 +
41 +
    // 3. Server modification for RPC export type (no client imports)
42 +
    const serverIndexPath = path.join(projectPath, "server", "src", "index.ts");
43 +
    await fs.writeFile(serverIndexPath, honoRpcTemplate, "utf8");
44 +
45 +
    // 4. Create separate client helper file
46 +
    const clientHelperPath = path.join(
47 +
      projectPath,
48 +
      "server",
49 +
      "src",
50 +
      "client.ts",
51 +
    );
52 +
    await fs.writeFile(clientHelperPath, honoClientTemplate, "utf8");
53 +
54 +
    // 5. Update App.tsx based on template selection using switch statement
55 +
    const appTsxPath = path.join(projectPath, "client", "src", "App.tsx");
56 +
57 +
    // Determine template content based on the template type
58 +
    let updatedAppContent: string;
59 +
60 +
    // Select template based on choice
61 +
    switch (templateChoice) {
62 +
      case "shadcn":
63 +
        updatedAppContent = shadcnTemplate;
64 +
        break;
65 +
      case "tailwind":
66 +
        updatedAppContent = tailwindTemplate;
67 +
        break;
68 +
      default:
69 +
        updatedAppContent = defaultTemplate;
70 +
        break;
71 +
    }
72 +
73 +
    await fs.writeFile(appTsxPath, updatedAppContent, "utf8");
74 +
    spinner.succeed("RPC client setup completed");
75 +
    return true;
76 +
  } catch (err: unknown) {
77 +
    spinner.fail("Failed to set up RPC client");
78 +
    if (err instanceof Error) {
79 +
      consola.error(pc.red("Error:"), err.message);
80 +
    } else {
81 +
      consola.error(pc.red("Error: Unknown error"));
82 +
    }
83 +
    return false;
84 +
  }
85 +
}
src/lib/setup-biome.ts (added) +85 −0
1 +
import path from "node:path";
2 +
import { execa } from "execa";
3 +
import fs from "fs-extra";
4 +
import ora from "ora";
5 +
import pc from "picocolors";
6 +
7 +
export async function setupBiome(projectPath: string): Promise<void> {
8 +
  const spinner = ora("Setting up Biome...").start();
9 +
  try {
10 +
    const clientPath = path.join(projectPath, "client");
11 +
    const clientPkgJsonPath = path.join(clientPath, "package.json");
12 +
    const eslintConfigPath = path.join(clientPath, "eslint.config.js");
13 +
14 +
    // Remove ESLint config file
15 +
    if (fs.existsSync(eslintConfigPath)) {
16 +
      await fs.remove(eslintConfigPath);
17 +
    }
18 +
19 +
    // Read client package.json and remove ESLint dependencies
20 +
    const clientPkgJson = await fs.readJson(clientPkgJsonPath);
21 +
    const devDependencies = clientPkgJson.devDependencies || {};
22 +
    const eslintDeps = Object.keys(devDependencies).filter(
23 +
      (dep) => dep.includes("eslint") || dep.includes("@eslint"),
24 +
    );
25 +
26 +
    if (eslintDeps.length > 0) {
27 +
      spinner.text = "Replacing ESLint dependencies...";
28 +
      await execa("bun", ["remove", ...eslintDeps], { cwd: clientPath });
29 +
    }
30 +
31 +
    // Install Biome in the root of the project
32 +
    spinner.text = "Installing Biome...";
33 +
    await execa("bun", ["add", "-D", "@biomejs/biome"], { cwd: projectPath });
34 +
35 +
    // Create biome.json in the root of the project
36 +
    spinner.text = "Creating biome.json...";
37 +
    const biomeConfig = {
38 +
      $schema: "https://biomejs.dev/schemas/1.7.3/schema.json",
39 +
      vcs: {
40 +
        enabled: true,
41 +
        clientKind: "git",
42 +
        useIgnoreFile: true,
43 +
      },
44 +
      files: { ignoreUnknown: false, ignore: [] },
45 +
      formatter: { enabled: true },
46 +
      organizeImports: { enabled: true },
47 +
      linter: {
48 +
        enabled: true,
49 +
        rules: {
50 +
          recommended: true,
51 +
        },
52 +
      },
53 +
    };
54 +
    const biomeConfigPath = path.join(projectPath, "biome.json");
55 +
    await fs.writeJson(biomeConfigPath, biomeConfig, { spaces: 2 });
56 +
57 +
    // Update client package.json scripts to remove lint
58 +
    spinner.text = "Updating scripts in client/package.json...";
59 +
    const newClientPkgJson = await fs.readJson(clientPkgJsonPath);
60 +
    if (newClientPkgJson.scripts || newClientPkgJson.scripts.lint) {
61 +
      delete newClientPkgJson.scripts.lint;
62 +
    }
63 +
    await fs.writeJson(clientPkgJsonPath, newClientPkgJson, { spaces: 2 });
64 +
65 +
    // Update root package.json with biome scripts
66 +
    spinner.text = "Updating scripts in root/package.json...";
67 +
    const rootPkgJsonPath = path.join(projectPath, "package.json");
68 +
    if (fs.existsSync(rootPkgJsonPath)) {
69 +
      const rootPkgJson = await fs.readJson(rootPkgJsonPath);
70 +
      rootPkgJson.scripts = rootPkgJson.scripts || {};
71 +
      rootPkgJson.scripts.format = "biome format . --write";
72 +
      rootPkgJson.scripts.lint = "biome lint .";
73 +
      await fs.writeJson(rootPkgJsonPath, rootPkgJson, { spaces: 2 });
74 +
    }
75 +
76 +
    spinner.succeed("Biome setup complete.");
77 +
  } catch (error) {
78 +
    spinner.fail("Biome setup failed.");
79 +
    if (error instanceof Error) {
80 +
      console.error(pc.red("\nError:"), error.message);
81 +
    } else {
82 +
      console.error(pc.red("\nError: Unknown error during Biome setup."));
83 +
    }
84 +
  }
85 +
}
src/program.ts (added) +3 −0
1 +
import { Command } from "commander";
2 +
3 +
export const program = new Command();
src/utils/constants.ts (added) +1 −0
1 +
export const DEFAULT_REPO = "stevedylandev/bhvr";
src/utils/helpers.ts (deleted) +0 −447
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 -
  honoClientTemplate,
10 -
  shadcnTemplate,
11 -
  tailwindTemplate,
12 -
  defaultTemplate,
13 -
  TEMPLATES,
14 -
} from "./templates";
15 -
import type { ProjectOptions, ProjectResult } from "../types";
16 -
import degit from "degit";
17 -
import prompts from "prompts";
18 -
19 -
export const DEFAULT_REPO = "stevedylandev/bhvr";
20 -
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 -
    });
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 -
  }
38 -
39 -
  console.log(`\n${chalk.cyan("🦫 Lets build 🦫")}\n`);
40 -
  console.log(`${chalk.blue("https://github.com/stevedylandev/bhvr")}\n`);
41 -
}
42 -
43 -
export async function patchFilesForRPC(
44 -
  projectPath: string,
45 -
  templateChoice: string,
46 -
): Promise<boolean> {
47 -
  const spinner = ora("Setting up RPC client...").start();
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);
53 -
54 -
    if (!clientPkg.dependencies.hono) {
55 -
      await execa("bun", ["install", "hono"], { cwd: projectPath });
56 -
    }
57 -
58 -
    await fs.writeJson(clientPkgPath, clientPkg, { spaces: 2 });
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);
63 -
64 -
    // Update the dev script to include TypeScript compilation
65 -
    serverPkg.scripts.dev = "bun --watch run src/index.ts && tsc --watch";
66 -
67 -
    await fs.writeJson(serverPkgPath, serverPkg, { spaces: 2 });
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");
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");
81 -
82 -
    // 5. Update App.tsx based on template selection using switch statement
83 -
    const appTsxPath = path.join(projectPath, "client", "src", "App.tsx");
84 -
85 -
    // Determine template content based on the template type
86 -
    let updatedAppContent: string;
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 -
    }
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 -
  }
113 -
}
114 -
115 -
export async function createProject(
116 -
  projectDirectory: string,
117 -
  options: ProjectOptions,
118 -
): Promise<ProjectResult | null> {
119 -
  let projectName = projectDirectory;
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 -
    });
128 -
129 -
    if (!response.projectName) {
130 -
      console.log(chalk.yellow("Project creation cancelled."));
131 -
      return null;
132 -
    }
133 -
134 -
    projectName = response.projectName;
135 -
  } else if (!projectName) {
136 -
    projectName = "my-bhvr-app";
137 -
  }
138 -
139 -
  let templateChoice = options.template || "default";
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 -
    }));
146 -
147 -
    const templateResponse = await prompts({
148 -
      type: "select",
149 -
      name: "template",
150 -
      message: "Select a template:",
151 -
      choices: templateChoices,
152 -
      initial: 0,
153 -
    });
154 -
155 -
    if (templateResponse.template === undefined) {
156 -
      console.log(chalk.yellow("Project creation cancelled."));
157 -
      return null;
158 -
    }
159 -
160 -
    templateChoice = templateResponse.template;
161 -
  }
162 -
163 -
  const projectPath = path.resolve(process.cwd(), projectName);
164 -
165 -
  if (fs.existsSync(projectPath)) {
166 -
    const files = fs.readdirSync(projectPath);
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 -
      });
175 -
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;
242 -
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 -
      });
254 -
255 -
      if (linterResponse.linter === undefined) {
256 -
        console.log(chalk.yellow("Project creation cancelled."));
257 -
        return null;
258 -
      }
259 -
260 -
      linter = linterResponse.linter;
261 -
    }
262 -
263 -
    if (linter === "biome") {
264 -
      await setupBiome(projectPath);
265 -
    }
266 -
267 -
    let gitInitialized = false;
268 -
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 -
      });
276 -
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 -
    }
304 -
305 -
    let dependenciesInstalled = false;
306 -
307 -
    if (!options.yes) {
308 -
      const depsResponse = await prompts({
309 -
        type: "confirm",
310 -
        name: "installDeps",
311 -
        message: "Install dependencies?",
312 -
        initial: true,
313 -
      });
314 -
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 -
    }
356 -
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 -
}
368 -
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");
375 -
376 -
    // Remove ESLint config file
377 -
    if (fs.existsSync(eslintConfigPath)) {
378 -
      await fs.remove(eslintConfigPath);
379 -
    }
380 -
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.includes("eslint") || dep.includes("@eslint"),
386 -
    );
387 -
388 -
    if (eslintDeps.length > 0) {
389 -
      spinner.text = "Replacing ESLint dependencies...";
390 -
      await execa("bun", ["remove", ...eslintDeps], { cwd: clientPath });
391 -
    }
392 -
393 -
    // Install Biome in the root of the project
394 -
    spinner.text = "Installing Biome...";
395 -
    await execa("bun", ["add", "-D", "@biomejs/biome"], { cwd: projectPath });
396 -
397 -
    // Create biome.json in the root of the project
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(projectPath, "biome.json");
417 -
    await fs.writeJson(biomeConfigPath, biomeConfig, { spaces: 2 });
418 -
419 -
    // Update client package.json scripts to remove lint
420 -
    spinner.text = "Updating scripts in client/package.json...";
421 -
    const newClientPkgJson = await fs.readJson(clientPkgJsonPath);
422 -
    if (newClientPkgJson.scripts || newClientPkgJson.scripts.lint) {
423 -
      delete newClientPkgJson.scripts.lint;
424 -
    }
425 -
    await fs.writeJson(clientPkgJsonPath, newClientPkgJson, { spaces: 2 });
426 -
427 -
    // Update root package.json with biome scripts
428 -
    spinner.text = "Updating scripts in root/package.json...";
429 -
    const rootPkgJsonPath = path.join(projectPath, "package.json");
430 -
    if (fs.existsSync(rootPkgJsonPath)) {
431 -
      const rootPkgJson = await fs.readJson(rootPkgJsonPath);
432 -
      rootPkgJson.scripts = rootPkgJson.scripts || {};
433 -
      rootPkgJson.scripts.format = "biome format . --write";
434 -
      rootPkgJson.scripts.lint = "biome lint .";
435 -
      await fs.writeJson(rootPkgJsonPath, rootPkgJson, { spaces: 2 });
436 -
    }
437 -
438 -
    spinner.succeed("Biome setup complete.");
439 -
  } catch (error) {
440 -
    spinner.fail("Biome setup failed.");
441 -
    if (error instanceof Error) {
442 -
      console.error(chalk.red("\nError:"), error.message);
443 -
    } else {
444 -
      console.error(chalk.red("\nError: Unknown error during Biome setup."));
445 -
    }
446 -
  }
447 -
}
src/utils/index.ts +3 −1
1 -
export * from "./helpers";
1 +
export * from "./constants";
2 2
export * from "./templates";
3 +
export * from "./try-catch";
4 +
src/utils/templates.ts +1 −1
1 -
import type { TemplateInfo } from "../types";
1 +
import type { TemplateInfo } from "@/types";
2 2
3 3
export const TEMPLATES: Record<string, TemplateInfo> = {
4 4
	default: {
src/utils/try-catch.ts (added) +23 −0
1 +
type Success<T> = {
2 +
  data: T;
3 +
  error: null;
4 +
};
5 +
6 +
type Failure<E> = {
7 +
  data: null;
8 +
  error: E;
9 +
};
10 +
11 +
type Result<T, E = Error> = Success<T> | Failure<E>;
12 +
13 +
// Main wrapper function
14 +
export async function tryCatch<T, E = Error>(
15 +
  promise: Promise<T>,
16 +
): Promise<Result<T, E>> {
17 +
  try {
18 +
    const data = await promise;
19 +
    return { data, error: null };
20 +
  } catch (error) {
21 +
    return { data: null, error: error as E };
22 +
  }
23 +
}
tsconfig.json +5 −1
21 21
		// Some stricter flags (disabled by default)
22 22
		"noUnusedLocals": false,
23 23
		"noUnusedParameters": false,
24 -
		"noPropertyAccessFromIndexSignature": false
24 +
		"noPropertyAccessFromIndexSignature": false,
25 +
    "baseUrl": ".",
26 +
    "paths": {
27 +
      "@/*": ["src/*"]
28 +
    }
25 29
	},
26 30
	"include": ["src/**/*"],
27 31
	"exclude": ["node_modules", "dist", "examples/**/*"]