Merge pull request #9 from 0xZ0uk/feat/tui-tweaks b5941aaa
feat: CLI and directory improvements
Steve Simkins · 2025-07-11 19:17 23 file(s) · +753 −603
CONTRIBUTING.md (added) +25 −0
1 +
# Contributing to create-bhvr
2 +
3 +
First off, thank you for considering contributing to `create-bhvr`.
4 +
5 +
## How Can I Contribute?
6 +
7 +
### Reporting Bugs
8 +
9 +
If you find a bug, please make sure the bug has not already been reported by searching on GitHub under [Issues](https://github.com/stevedylandev/bhvr/issues). If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/stevedylandev/bhvr/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
10 +
11 +
### Suggesting Enhancements
12 +
13 +
If you have an idea for an enhancement, please make sure the enhancement has not already been suggested by searching on GitHub under [Issues](https://github.com/stevedylandev/bhvr/issues). If you're unable to find an open issue addressing the suggestion, [open a new one](https://github.com/stevedylandev/bhvr/issues/new). Be sure to include a **title and clear description** of the enhancement you're suggesting.
14 +
15 +
### Submitting a Pull Request
16 +
17 +
1.  Fork the repository and create your branch from `main`.
18 +
2.  Run `bun install` to install the dependencies.
19 +
3.  Make your changes.
20 +
4.  Run `bun run build` to make sure your changes build correctly.
21 +
5.  Issue that pull request!
22 +
23 +
## License
24 +
25 +
By contributing, you agree that your contributions will be licensed under its MIT License.
README.md (added) +55 −0
1 +
# create-bhvr 🦫
2 +
3 +
![cover](https://cdn.stevedylan.dev/ipfs/bafybeievx27ar5qfqyqyud7kemnb5n2p4rzt2matogi6qttwkpxonqhra4)
4 +
5 +
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/stevedylandev/create-bhvr/blob/main/LICENSE)
6 +
![create-bhvr version](https://img.shields.io/npm/v/create-bhvr.svg?label=%20)
7 +
8 +
A command-line interface (CLI) to quickly scaffold a new `bhvr` project. `bhvr` is a full-stack TypeScript monorepo starter with shared types, using Bun, Hono, Vite, and React.
9 +
10 +
## Getting Started
11 +
12 +
To create a new `bhvr` project, run any of the following commands and follow the interactive prompts:
13 +
14 +
```bash
15 +
# Using Bun
16 +
bun create bhvr@latest my-bhvr-app
17 +
```
18 +
19 +
This will create a new directory called `my-bhvr-app` inside the current folder.
20 +
21 +
## Features
22 +
23 +
- **Interactive Setup**: A simple and fast interactive CLI to guide you through project setup.
24 +
- **Multiple Templates**: Choose from several templates to get started:
25 +
    - `default`: A basic setup with Bun, Hono, Vite, and React.
26 +
    - `tailwind`: Includes Tailwind CSS for styling.
27 +
    - `shadcn`: Pre-configured with Tailwind CSS and shadcn/ui.
28 +
- **Optional RPC**: Automatically configure Hono RPC for end-to-end type-safe API communication.
29 +
- **Linter Choice**: Choose between ESLint (default) or Biome for code linting and formatting.
30 +
- **Automated Setup**: Handles `git` initialization and dependency installation for you.
31 +
32 +
## Command-Line Options
33 +
34 +
You can also use command-line options to skip the interactive prompts:
35 +
36 +
| Option                  | Description                                            | Default   |
37 +
| ----------------------- | ------------------------------------------------------ | --------- |
38 +
| `[project-directory]`   | The name of the directory to create the project in.    | -         |
39 +
| `-y, --yes`             | Skip all confirmation prompts and use default values.  | `false`   |
40 +
| `--template <template>` | Specify a template (`default`, `tailwind`, `shadcn`).  | `default` |
41 +
| `--rpc`                 | Use Hono RPC for type-safe API communication.          | `false`   |
42 +
| `--linter <linter>`     | Specify the linter to use (`eslint` or `biome`).       | `eslint`  |
43 +
| `--branch <branch>`     | Specify a branch to use from the repository.           | `main`    |
44 +
45 +
## Contributing
46 +
47 +
We welcome contributions from the community! Whether it's reporting a bug, suggesting a new feature, or submitting a pull request, your help is appreciated.
48 +
49 +
Please read our [**CONTRIBUTING.md**](CONTRIBUTING.md) for detailed guidelines on how to get started.
50 +
51 +
## Links
52 +
53 +
- [License (MIT)](LICENSE)
54 +
- [Contributing](CONTRIBUTING.md)
55 +
- [Website](https://bhvr.dev)
bun.lock +10 −56
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 -
        "ora": "^6.3.1",
14 -
        "prompts": "^2.4.2",
13 +
        "picocolors": "^1.1.1",
14 +
        "yocto-spinner": "^1.0.0",
15 15
      },
16 16
      "devDependencies": {
17 17
        "@types/degit": "^2.8.6",
62 62
63 63
    "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
64 64
65 -
    "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
66 -
67 65
    "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="],
68 66
69 -
    "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
67 +
    "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
70 68
71 -
    "bl": ["bl@5.1.0", "", { "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ=="],
72 -
73 -
    "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
74 -
75 -
    "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
76 -
77 -
    "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
78 -
79 -
    "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
80 -
81 -
    "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
82 -
83 -
    "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
69 +
    "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
84 70
85 71
    "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="],
86 72
87 73
    "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
88 74
89 -
    "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
90 -
91 75
    "degit": ["degit@2.8.4", "", { "bin": { "degit": "degit" } }, "sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng=="],
92 76
93 77
    "diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="],
104 88
105 89
    "human-signals": ["human-signals@4.3.1", "", {}, "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ=="],
106 90
107 -
    "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
108 -
109 -
    "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
110 -
111 -
    "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
112 -
113 91
    "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
114 92
115 -
    "is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
116 -
117 93
    "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
118 94
119 95
    "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
120 96
121 97
    "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
122 -
123 -
    "log-symbols": ["log-symbols@5.1.0", "", { "dependencies": { "chalk": "^5.0.0", "is-unicode-supported": "^1.1.0" } }, "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA=="],
124 98
125 99
    "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="],
126 100
131 105
    "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="],
132 106
133 107
    "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
134 -
135 -
    "ora": ["ora@6.3.1", "", { "dependencies": { "chalk": "^5.0.0", "cli-cursor": "^4.0.0", "cli-spinners": "^2.6.1", "is-interactive": "^2.0.0", "is-unicode-supported": "^1.1.0", "log-symbols": "^5.1.0", "stdin-discarder": "^0.1.0", "strip-ansi": "^7.0.1", "wcwidth": "^1.0.1" } }, "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ=="],
136 108
137 109
    "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
138 110
139 -
    "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
140 -
141 -
    "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 -
143 -
    "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
111 +
    "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
144 112
145 113
    "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
146 -
147 -
    "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
148 114
149 115
    "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
150 116
152 118
153 119
    "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
154 120
155 -
    "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
156 -
157 -
    "stdin-discarder": ["stdin-discarder@0.1.0", "", { "dependencies": { "bl": "^5.0.0" } }, "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ=="],
158 -
159 -
    "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
160 -
161 -
    "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
162 -
163 121
    "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
164 122
165 123
    "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="],
172 130
173 131
    "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
174 132
175 -
    "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
176 -
177 133
    "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="],
178 -
179 -
    "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
180 134
181 135
    "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
182 136
183 137
    "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="],
184 138
139 +
    "yocto-spinner": ["yocto-spinner@1.0.0", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-VPX8P/+Z2Fnpx8PC/JELbxp3QRrBxjAekio6yulGtA5gKt9YyRc5ycCb+NHgZCbZ0kx9KxwZp7gC6UlrCcCdSQ=="],
140 +
141 +
    "yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="],
142 +
185 143
    "@types/fs-extra/@types/node": ["@types/node@20.17.47", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ=="],
186 144
187 145
    "@types/jsonfile/@types/node": ["@types/node@20.17.47", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ=="],
192 150
193 151
    "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
194 152
195 -
    "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
196 -
197 153
    "@types/fs-extra/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
198 154
199 155
    "@types/jsonfile/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
201 157
    "@types/prompts/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
202 158
203 159
    "@types/through/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
204 -
205 -
    "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
206 160
  }
207 161
}
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 +
    "picocolors": "^1.1.1",
48 +
    "yocto-spinner": "^1.0.0"
49 +
  },
50 +
  "engines": {
51 +
    "node": ">=18"
52 +
  }
53 53
}
src/commands/create.ts (added) +35 −0
1 +
import pc from "picocolors";
2 +
import { displayBanner } from "@/lib/display-banner";
3 +
import type { ProjectOptions } from "@/types";
4 +
import { createProject } from "../lib/create-project";
5 +
6 +
export const create = async (
7 +
  projectDirectory: string,
8 +
  options: ProjectOptions,
9 +
) => {
10 +
  try {
11 +
    displayBanner();
12 +
    const result = await createProject(projectDirectory, options);
13 +
    if (result) {
14 +
      console.log(pc.green(pc.bold("🎉 Project created successfully!")));
15 +
      console.log("\nNext steps:");
16 +
      if (!result.dependenciesInstalled) {
17 +
        console.log(pc.cyan(`  cd ${result.projectName}`));
18 +
        console.log(pc.cyan("  bun install"));
19 +
      } else {
20 +
        console.log(pc.cyan(`  cd ${result.projectName}`));
21 +
      }
22 +
      console.log(pc.cyan("  bun run dev:client   # Start the client"));
23 +
      console.log(
24 +
        pc.cyan(
25 +
          "  bun run dev:server   # Start the server in another terminal",
26 +
        ),
27 +
      );
28 +
      console.log(pc.cyan("  bun run dev          # Start all"));
29 +
      process.exit(0);
30 +
    }
31 +
  } catch (err) {
32 +
    console.error(pc.red("Error creating project:"), err);
33 +
    process.exit(1);
34 +
  }
35 +
};
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) +42 −0
1 +
import type { ProjectOptions, ProjectResult } from "@/types";
2 +
import { initializeGit } from "./initialize-git";
3 +
import { installDependencies } from "./install-dependencies";
4 +
import { promptForOptions } from "./prompt-for-options";
5 +
import { scaffoldTemplate } from "./scaffold-template";
6 +
7 +
export async function createProject(
8 +
  projectDirectory: string,
9 +
  options: ProjectOptions,
10 +
): Promise<ProjectResult | null> {
11 +
  const projectOptions = await promptForOptions({
12 +
    ...options,
13 +
    projectName: projectDirectory,
14 +
  });
15 +
16 +
  if (!projectOptions) {
17 +
    return null;
18 +
  }
19 +
20 +
  const scaffolded = await scaffoldTemplate(projectOptions);
21 +
22 +
  if (!scaffolded) {
23 +
    return null;
24 +
  }
25 +
26 +
  const gitInitialized = await initializeGit(
27 +
    projectOptions.projectName ?? projectDirectory,
28 +
    projectOptions.yes,
29 +
  );
30 +
31 +
  const dependenciesInstalled = await installDependencies(
32 +
    projectOptions.projectName ?? projectDirectory,
33 +
    projectOptions.yes,
34 +
  );
35 +
36 +
  return {
37 +
    projectName: projectOptions.projectName ?? projectDirectory,
38 +
    gitInitialized,
39 +
    dependenciesInstalled,
40 +
    template: projectOptions.template ?? "default",
41 +
  };
42 +
}
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) +8 −0
1 +
export * from "./create-project";
2 +
export * from "./display-banner";
3 +
export * from "./initialize-git";
4 +
export * from "./install-dependencies";
5 +
export * from "./patch-files-rpc";
6 +
export * from "./prompt-for-options";
7 +
export * from "./scaffold-template";
8 +
export * from "./setup-biome";
src/lib/initialize-git.ts (added) +47 −0
1 +
import { consola } from "consola";
2 +
import { execa } from "execa";
3 +
import pc from "picocolors";
4 +
import yoctoSpinner from "yocto-spinner";
5 +
import { tryCatch } from "@/utils/try-catch";
6 +
7 +
export async function initializeGit(
8 +
  projectPath: string,
9 +
  skipConfirmation?: boolean,
10 +
): Promise<boolean> {
11 +
  if (!skipConfirmation) {
12 +
    const { data: gitResponse, error } = await tryCatch(
13 +
      consola.prompt("Initialize a git repository?", {
14 +
        type: "confirm",
15 +
        initial: true,
16 +
        cancel: "reject",
17 +
      }),
18 +
    );
19 +
20 +
    if (error) {
21 +
      console.log(pc.yellow("Project creation cancelled."));
22 +
      return false;
23 +
    }
24 +
25 +
    if (!gitResponse) {
26 +
      return false;
27 +
    }
28 +
  }
29 +
30 +
  const spinner = yoctoSpinner({
31 +
    text: "Initializing git repository...",
32 +
  }).start();
33 +
34 +
  try {
35 +
    await execa("git", ["init"], { cwd: projectPath });
36 +
    spinner.success("Git repository initialized");
37 +
    return true;
38 +
  } catch (err: unknown) {
39 +
    spinner.error("Failed to initialize git repository. Is git installed?");
40 +
    if (err instanceof Error) {
41 +
      consola.error(pc.red("Git error:"), err.message);
42 +
    } else {
43 +
      consola.error(pc.red("Git error: Unknown error"));
44 +
    }
45 +
    return false;
46 +
  }
47 +
}
src/lib/install-dependencies.ts (added) +61 −0
1 +
import { consola } from "consola";
2 +
import { execa } from "execa";
3 +
import pc from "picocolors";
4 +
import yoctoSpinner from "yocto-spinner";
5 +
import { tryCatch } from "@/utils/try-catch";
6 +
7 +
async function getPackageManager(): Promise<"bun"> {
8 +
  const { error } = await tryCatch(execa("bun", ["--version"]));
9 +
10 +
  if (error) {
11 +
    consola.error(new Error("Bun is not installed."));
12 +
    consola.warn("Please install bun from https://bun.sh/");
13 +
    process.exit(1);
14 +
  }
15 +
16 +
  return "bun";
17 +
}
18 +
19 +
export async function installDependencies(
20 +
  projectPath: string,
21 +
  skipConfirmation?: boolean,
22 +
): Promise<boolean> {
23 +
  if (!skipConfirmation) {
24 +
    const { data: depsResponse, error } = await tryCatch(
25 +
      consola.prompt("Install dependencies?", {
26 +
        type: "confirm",
27 +
        initial: true,
28 +
        cancel: "reject",
29 +
      }),
30 +
    );
31 +
32 +
    if (error) {
33 +
      console.log(pc.yellow("Project creation cancelled."));
34 +
      return false;
35 +
    }
36 +
37 +
    if (!depsResponse) {
38 +
      return false;
39 +
    }
40 +
  }
41 +
42 +
  const packageManager = await getPackageManager();
43 +
44 +
  const spinner = yoctoSpinner({
45 +
    text: `Installing dependencies with ${packageManager}...`,
46 +
  }).start();
47 +
48 +
  try {
49 +
    await execa(packageManager, ["install"], { cwd: projectPath });
50 +
    spinner.success(`Dependencies installed with ${packageManager}`);
51 +
    return true;
52 +
  } catch (_err) {
53 +
    spinner.error("Failed to install dependencies.");
54 +
    console.log(
55 +
      pc.yellow(
56 +
        "You can install them manually after navigating to the project directory.",
57 +
      ),
58 +
    );
59 +
    return false;
60 +
  }
61 +
}
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 pc from "picocolors";
6 +
import yoctoSpinner from "yocto-spinner";
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 = yoctoSpinner({ text: "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.success("RPC client setup completed");
75 +
    return true;
76 +
  } catch (err: unknown) {
77 +
    spinner.error("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/prompt-for-options.ts (added) +103 −0
1 +
import { consola } from "consola";
2 +
import pc from "picocolors";
3 +
import type { ProjectOptions } from "@/types";
4 +
import { TEMPLATES } from "@/utils/templates";
5 +
import { tryCatch } from "@/utils/try-catch";
6 +
7 +
export async function promptForOptions(
8 +
  options: ProjectOptions,
9 +
): Promise<ProjectOptions> {
10 +
  let projectName = options.projectName;
11 +
12 +
  if (!projectName && !options.yes) {
13 +
    const { data, error } = await tryCatch(
14 +
      consola.prompt(pc.yellow("What is the name of your project?"), {
15 +
        type: "text",
16 +
        default: "my-bhvr-app",
17 +
        placeholder: "my-bhvr-app",
18 +
        cancel: "reject",
19 +
      }),
20 +
    );
21 +
22 +
    if (!data || error) {
23 +
      consola.error(pc.red("Project creation cancelled."));
24 +
      process.exit(1);
25 +
    }
26 +
27 +
    projectName = data;
28 +
  }
29 +
30 +
  let templateChoice = options.template || "default";
31 +
32 +
  if (!options.yes && !options.branch) {
33 +
    const templateChoices = Object.keys(TEMPLATES).map((key) => ({
34 +
      label: `${key} (${TEMPLATES[key]?.description})`,
35 +
      value: key,
36 +
    }));
37 +
38 +
    const { data, error } = await tryCatch(
39 +
      consola.prompt(pc.yellow("Select a template:"), {
40 +
        type: "select",
41 +
        options: templateChoices,
42 +
        initial: "default",
43 +
        cancel: "reject",
44 +
      }),
45 +
    );
46 +
47 +
    if (!data || error) {
48 +
      consola.error("Project creation cancelled.");
49 +
      process.exit(1);
50 +
    }
51 +
52 +
    templateChoice = data;
53 +
  }
54 +
55 +
  let useRpc = options.rpc;
56 +
57 +
  if (!options.yes && !options.rpc) {
58 +
    const { data: rpcResponse, error } = await tryCatch(
59 +
      consola.prompt("Use Hono RPC client for type-safe API communication?", {
60 +
        type: "confirm",
61 +
        initial: false,
62 +
      }),
63 +
    );
64 +
65 +
    if (error) {
66 +
      consola.error("Project creation cancelled.");
67 +
      process.exit(1);
68 +
    }
69 +
70 +
    useRpc = rpcResponse;
71 +
  }
72 +
73 +
  let linter = options.linter;
74 +
75 +
  if (!options.yes && !options.linter) {
76 +
    const { data: linterResponse, error } = await tryCatch(
77 +
      consola.prompt("Select a linter:", {
78 +
        type: "select",
79 +
        options: [
80 +
          { label: "ESLint (default)", value: "eslint" },
81 +
          { label: "Biome", value: "biome" },
82 +
        ],
83 +
        initial: "eslint",
84 +
        cancel: "reject",
85 +
      }),
86 +
    );
87 +
88 +
    if (error) {
89 +
      console.log(pc.yellow("Project creation cancelled."));
90 +
      process.exit(1);
91 +
    }
92 +
93 +
    linter = linterResponse as "eslint" | "biome";
94 +
  }
95 +
96 +
  return {
97 +
    ...options,
98 +
    projectName,
99 +
    template: templateChoice,
100 +
    rpc: useRpc,
101 +
    linter,
102 +
  };
103 +
}
src/lib/scaffold-template.ts (added) +71 −0
1 +
import path from "node:path";
2 +
import degit from "degit";
3 +
import fs from "fs-extra";
4 +
import pc from "picocolors";
5 +
import yoctoSpinner from "yocto-spinner";
6 +
import type { ProjectOptions } from "@/types";
7 +
import { DEFAULT_REPO } from "@/utils/constants";
8 +
import { TEMPLATES } from "@/utils/templates";
9 +
import { patchFilesForRPC } from "./patch-files-rpc";
10 +
import { setupBiome } from "./setup-biome";
11 +
12 +
export async function scaffoldTemplate(
13 +
  options: Required<ProjectOptions>,
14 +
): Promise<boolean> {
15 +
  const { projectName, template, repo, branch, rpc, linter } = options;
16 +
17 +
  const projectPath = path.resolve(process.cwd(), projectName);
18 +
19 +
  if (fs.existsSync(projectPath)) {
20 +
    const files = fs.readdirSync(projectPath);
21 +
    if (files.length > 0) {
22 +
      await fs.emptyDir(projectPath);
23 +
    }
24 +
  }
25 +
26 +
  fs.ensureDirSync(projectPath);
27 +
28 +
  const repoPath = repo || DEFAULT_REPO;
29 +
  const templateConfig =
30 +
    TEMPLATES[template as keyof typeof TEMPLATES] || TEMPLATES.default;
31 +
  const repoBranch = branch || (templateConfig?.branch ?? "main");
32 +
  const repoUrl = `${repoPath}#${repoBranch}`;
33 +
  const spinner = yoctoSpinner({ text: "Downloading template..." }).start();
34 +
35 +
  try {
36 +
    const emitter = degit(repoUrl, {
37 +
      cache: false,
38 +
      force: true,
39 +
      verbose: false,
40 +
    });
41 +
42 +
    await emitter.clone(projectPath);
43 +
    spinner.success(`Template downloaded successfully (${template} template)`);
44 +
45 +
    const pkgJsonPath = path.join(projectPath, "package.json");
46 +
    if (fs.existsSync(pkgJsonPath)) {
47 +
      const pkgJson = await fs.readJson(pkgJsonPath);
48 +
      pkgJson.name = projectName;
49 +
      await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
50 +
    }
51 +
52 +
    const gitDir = path.join(projectPath, ".git");
53 +
    if (fs.existsSync(gitDir)) {
54 +
      await fs.remove(gitDir);
55 +
      console.log(pc.blue("Removed .git directory"));
56 +
    }
57 +
58 +
    if (rpc) {
59 +
      await patchFilesForRPC(projectPath, template);
60 +
    }
61 +
62 +
    if (linter === "biome") {
63 +
      await setupBiome(projectPath);
64 +
    }
65 +
66 +
    return true;
67 +
  } catch (err) {
68 +
    spinner.error("Failed to download template");
69 +
    throw err;
70 +
  }
71 +
}
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 pc from "picocolors";
5 +
import yoctoSpinner from "yocto-spinner";
6 +
7 +
export async function setupBiome(projectPath: string): Promise<void> {
8 +
  const spinner = yoctoSpinner({ text: "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.success("Biome setup complete.");
77 +
  } catch (error) {
78 +
    spinner.error("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/types.ts +14 −13
1 1
export interface TemplateInfo {
2 -
	branch: string;
3 -
	description: string;
2 +
  branch: string;
3 +
  description: string;
4 4
}
5 5
6 6
export type ProjectOptions = {
7 -
	yes?: boolean;
8 -
	typescript?: boolean;
9 -
	repo?: string;
10 -
	template?: string;
11 -
	branch?: string;
12 -
	rpc?: boolean;
13 -
	linter?: 'eslint' | 'biome';
7 +
  projectName?: string;
8 +
  yes?: boolean;
9 +
  typescript?: boolean;
10 +
  repo?: string;
11 +
  template?: string;
12 +
  branch?: string;
13 +
  rpc?: boolean;
14 +
  linter?: "eslint" | "biome";
14 15
};
15 16
16 17
export interface ProjectResult {
17 -
	projectName: string;
18 -
	gitInitialized: boolean;
19 -
	dependenciesInstalled: boolean;
20 -
	template: string;
18 +
  projectName: string;
19 +
  gitInitialized: boolean;
20 +
  dependenciesInstalled: boolean;
21 +
  template: string;
21 22
}
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/**/*"]