basuicn 0.3.11 → 0.3.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/dist/assets/abap-CLvhMVsD.js +1 -0
  2. package/dist/assets/actionscript-3--17pq3dv.js +1 -0
  3. package/dist/assets/ada-C5qYipkI.js +1 -0
  4. package/dist/assets/andromeeda-vGVdxbeo.js +1 -0
  5. package/dist/assets/angular-html-C_R4boCs.js +1 -0
  6. package/dist/assets/angular-ts-DDC-7KEU.js +1 -0
  7. package/dist/assets/apache-U0d_L8uA.js +1 -0
  8. package/dist/assets/apex-VAyPSnFM.js +1 -0
  9. package/dist/assets/apl-C6NMFcit.js +1 -0
  10. package/dist/assets/applescript-CCn79oCD.js +1 -0
  11. package/dist/assets/ara-4CJ0cIlV.js +1 -0
  12. package/dist/assets/asciidoc-DE70LPWp.js +1 -0
  13. package/dist/assets/asm-Cmm7eHzH.js +1 -0
  14. package/dist/assets/astro-D6HwFgiT.js +1 -0
  15. package/dist/assets/aurora-x-CDeNXAV0.js +1 -0
  16. package/dist/assets/awk-BWXHIvNe.js +1 -0
  17. package/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
  18. package/dist/assets/ayu-light-C3h-C4tm.js +1 -0
  19. package/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  20. package/dist/assets/ballerina-B7ZEbQpA.js +1 -0
  21. package/dist/assets/bat-Bo4NYOV-.js +1 -0
  22. package/dist/assets/beancount-D-usSTwE.js +1 -0
  23. package/dist/assets/berry-DKpUyyne.js +1 -0
  24. package/dist/assets/bibtex-Ci_nEsc7.js +1 -0
  25. package/dist/assets/bicep-CUHmPFLl.js +1 -0
  26. package/dist/assets/bird2-C6vDhewU.js +1 -0
  27. package/dist/assets/blade-C4l1V02K.js +1 -0
  28. package/dist/assets/bsl-BkkzgIyY.js +1 -0
  29. package/dist/assets/c-KSOZ_xJw.js +1 -0
  30. package/dist/assets/c3-BFHwR3_K.js +1 -0
  31. package/dist/assets/cadence-CQ2zXKGN.js +1 -0
  32. package/dist/assets/cairo-DLTphjLi.js +1 -0
  33. package/dist/assets/catppuccin-frappe-3VR1Za6u.js +1 -0
  34. package/dist/assets/catppuccin-latte-DwIHMF0Q.js +1 -0
  35. package/dist/assets/catppuccin-macchiato-DYnBP6_5.js +1 -0
  36. package/dist/assets/catppuccin-mocha-DYhrFGRu.js +1 -0
  37. package/dist/assets/clarity-SemFz856.js +1 -0
  38. package/dist/assets/clojure-DqKBuwfJ.js +1 -0
  39. package/dist/assets/cmake-Bj61d0ZC.js +1 -0
  40. package/dist/assets/cobol-DV3uoHgO.js +1 -0
  41. package/dist/assets/codeowners-C8r90Shi.js +1 -0
  42. package/dist/assets/codeql-oeQT6MSM.js +1 -0
  43. package/dist/assets/coffee-BI7IzQLX.js +1 -0
  44. package/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
  45. package/dist/assets/coq-BrsZFFmf.js +1 -0
  46. package/dist/assets/cpp-nwtcw6V-.js +1 -0
  47. package/dist/assets/crystal-Cj7dDBIL.js +1 -0
  48. package/dist/assets/csharp-oqKa8noW.js +1 -0
  49. package/dist/assets/css-Bjl9g7PY.js +1 -0
  50. package/dist/assets/csv-Dx-8-gkx.js +1 -0
  51. package/dist/assets/cue-CE9AQfxI.js +1 -0
  52. package/dist/assets/cypher-ClKdZ_lG.js +1 -0
  53. package/dist/assets/d-qD-0Kul2.js +1 -0
  54. package/dist/assets/dark-plus-Cs2F2srj.js +1 -0
  55. package/dist/assets/dart-CnvKMtbv.js +1 -0
  56. package/dist/assets/dax-BkyTk9wS.js +1 -0
  57. package/dist/assets/desktop-Dlh5hvp9.js +1 -0
  58. package/dist/assets/diff-woXpYk--.js +1 -0
  59. package/dist/assets/docker-IyjqRm3v.js +1 -0
  60. package/dist/assets/dotenv-_5a1GRtc.js +1 -0
  61. package/dist/assets/dracula-BHWKrbxM.js +1 -0
  62. package/dist/assets/dracula-soft-5eyTD99u.js +1 -0
  63. package/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
  64. package/dist/assets/edge-CLyulaNo.js +1 -0
  65. package/dist/assets/elixir-D1qe5nvJ.js +1 -0
  66. package/dist/assets/elm-BFVadj03.js +1 -0
  67. package/dist/assets/emacs-lisp-B4R74twV.js +1 -0
  68. package/dist/assets/erb-DLGpISTK.js +1 -0
  69. package/dist/assets/erlang-Cphh6RMH.js +1 -0
  70. package/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
  71. package/dist/assets/everforest-light-Df2xbC6M.js +1 -0
  72. package/dist/assets/fennel-DQxkIbk2.js +1 -0
  73. package/dist/assets/fish-BJitypiv.js +1 -0
  74. package/dist/assets/fluent-C03EYrpw.js +1 -0
  75. package/dist/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
  76. package/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
  77. package/dist/assets/fsharp-D13ZGOAj.js +1 -0
  78. package/dist/assets/gdresource-C0sCabJj.js +1 -0
  79. package/dist/assets/gdscript-Cp2uCuqX.js +1 -0
  80. package/dist/assets/gdshader-CBce3t8t.js +1 -0
  81. package/dist/assets/genie-CV2tkWYe.js +1 -0
  82. package/dist/assets/gherkin-DExj1W_8.js +1 -0
  83. package/dist/assets/git-commit-BSykSTBG.js +1 -0
  84. package/dist/assets/git-rebase-BAH2F1ja.js +1 -0
  85. package/dist/assets/github-dark-C-LZuMrd.js +1 -0
  86. package/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
  87. package/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  88. package/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  89. package/dist/assets/github-light-EUqPIrTm.js +1 -0
  90. package/dist/assets/github-light-default-BXViO-2h.js +1 -0
  91. package/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  92. package/dist/assets/gleam-CSRkHgEL.js +1 -0
  93. package/dist/assets/glimmer-js-Dd1uIqub.js +1 -0
  94. package/dist/assets/glimmer-ts-DjqoMFcz.js +1 -0
  95. package/dist/assets/glsl-D2nmnYHV.js +1 -0
  96. package/dist/assets/gn-ilITqXS6.js +1 -0
  97. package/dist/assets/gnuplot-7GGW24-e.js +1 -0
  98. package/dist/assets/go-rLFTqkRN.js +1 -0
  99. package/dist/assets/graphql-Cw77QnDJ.js +1 -0
  100. package/dist/assets/groovy-CacY0gHj.js +1 -0
  101. package/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  102. package/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  103. package/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  104. package/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  105. package/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  106. package/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  107. package/dist/assets/hack-Bb9mpV0i.js +1 -0
  108. package/dist/assets/haml-CF8RwEu1.js +1 -0
  109. package/dist/assets/handlebars-D2zFsrot.js +1 -0
  110. package/dist/assets/haskell-D8IpX4py.js +1 -0
  111. package/dist/assets/haxe-OTjmBuCE.js +1 -0
  112. package/dist/assets/hcl-Dh228itO.js +1 -0
  113. package/dist/assets/hjson-CxZEssPk.js +1 -0
  114. package/dist/assets/hlsl-Cvrh5tZx.js +1 -0
  115. package/dist/assets/horizon-CE9ld1lL.js +1 -0
  116. package/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
  117. package/dist/assets/houston-CsvMBhTu.js +1 -0
  118. package/dist/assets/html-DPsTyQ4s.js +1 -0
  119. package/dist/assets/html-derivative-uE_H-K2_.js +1 -0
  120. package/dist/assets/http-BLC6NeYh.js +1 -0
  121. package/dist/assets/hurl-DO46mQZ1.js +1 -0
  122. package/dist/assets/hxml-B0Qn7Nwc.js +1 -0
  123. package/dist/assets/hy-CZbG8q4J.js +1 -0
  124. package/dist/assets/imba-DsUTQ-LC.js +1 -0
  125. package/dist/assets/index-Ba666nDd.css +1 -0
  126. package/dist/assets/index-CnNrrRnN.js +897 -0
  127. package/dist/assets/ini-B5eOa1yu.js +1 -0
  128. package/dist/assets/java-kzUtURfb.js +1 -0
  129. package/dist/assets/javascript-hXYTfjcT.js +1 -0
  130. package/dist/assets/jinja-DW3Ipkk9.js +1 -0
  131. package/dist/assets/jison-Dtk5Sh4F.js +1 -0
  132. package/dist/assets/json-yvvy5IcZ.js +1 -0
  133. package/dist/assets/json5-BR5RXkoi.js +1 -0
  134. package/dist/assets/jsonc-CYpm1nAK.js +1 -0
  135. package/dist/assets/jsonl-CmCQp5Yx.js +1 -0
  136. package/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
  137. package/dist/assets/jssm-DXw9l8Rf.js +1 -0
  138. package/dist/assets/jsx-CXY-Xklo.js +1 -0
  139. package/dist/assets/julia-B-djpp87.js +1 -0
  140. package/dist/assets/just-Dy_P1Mi8.js +1 -0
  141. package/dist/assets/kanagawa-dragon-CXtmUGW6.js +1 -0
  142. package/dist/assets/kanagawa-lotus-BN08jTvb.js +1 -0
  143. package/dist/assets/kanagawa-wave-CTweb8Dz.js +1 -0
  144. package/dist/assets/kdl-CsD5j6eV.js +1 -0
  145. package/dist/assets/kotlin-DhhofPvG.js +1 -0
  146. package/dist/assets/kusto-C7mF5XQf.js +1 -0
  147. package/dist/assets/laserwave-C_8bwKvT.js +1 -0
  148. package/dist/assets/latex-CeRzDjwd.js +1 -0
  149. package/dist/assets/lean-CewbzKMR.js +1 -0
  150. package/dist/assets/less-DVTAwKKz.js +1 -0
  151. package/dist/assets/light-plus-DVQuIRkW.js +1 -0
  152. package/dist/assets/liquid-C5oaVhR-.js +1 -0
  153. package/dist/assets/llvm-Cm23YOpf.js +1 -0
  154. package/dist/assets/log-BNLmms1o.js +1 -0
  155. package/dist/assets/logo-Cluzi2Zq.js +1 -0
  156. package/dist/assets/lua-CzZee3zd.js +1 -0
  157. package/dist/assets/luau-FMPmPwt6.js +1 -0
  158. package/dist/assets/make-Dixweg8N.js +1 -0
  159. package/dist/assets/markdown-BYOwaDjH.js +1 -0
  160. package/dist/assets/marko-DwJR36iE.js +1 -0
  161. package/dist/assets/material-theme-Bm3Qr25_.js +1 -0
  162. package/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
  163. package/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
  164. package/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  165. package/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  166. package/dist/assets/matlab-D7qyCx1q.js +1 -0
  167. package/dist/assets/mdc-Cz5KPxip.js +1 -0
  168. package/dist/assets/mdx-DQZ5AkYe.js +1 -0
  169. package/dist/assets/mermaid-Bk4SNUv9.js +1 -0
  170. package/dist/assets/min-dark-BSWPekZh.js +1 -0
  171. package/dist/assets/min-light-DDpmG2fV.js +1 -0
  172. package/dist/assets/mipsasm-BMqwQI7S.js +1 -0
  173. package/dist/assets/mojo-BgCJLMeH.js +1 -0
  174. package/dist/assets/monokai-CdkpiU2Y.js +1 -0
  175. package/dist/assets/moonbit-CaWjb8XO.js +1 -0
  176. package/dist/assets/move-B1IS1UjX.js +1 -0
  177. package/dist/assets/narrat-_X_XdTYD.js +1 -0
  178. package/dist/assets/nextflow-BJtWHP5T.js +1 -0
  179. package/dist/assets/nextflow-groovy-DJMQeKeT.js +1 -0
  180. package/dist/assets/nginx-DP8PKHqg.js +1 -0
  181. package/dist/assets/night-owl-DhmEMT88.js +1 -0
  182. package/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
  183. package/dist/assets/nim-DLj_luda.js +1 -0
  184. package/dist/assets/nix-IvuFDN5E.js +1 -0
  185. package/dist/assets/nord-Cb4Vim4T.js +1 -0
  186. package/dist/assets/nushell-DcLAeLz5.js +1 -0
  187. package/dist/assets/objective-c-D1A_Heim.js +1 -0
  188. package/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
  189. package/dist/assets/ocaml-O90oeIOV.js +1 -0
  190. package/dist/assets/odin-B1RWQWA5.js +1 -0
  191. package/dist/assets/one-dark-pro-CLwyXe_n.js +1 -0
  192. package/dist/assets/one-light-D7Lr4KcI.js +1 -0
  193. package/dist/assets/openscad-BUDT5pXO.js +1 -0
  194. package/dist/assets/pascal-4ZHwLPI5.js +1 -0
  195. package/dist/assets/perl-gt0YvaTf.js +1 -0
  196. package/dist/assets/php-BPUC88QF.js +1 -0
  197. package/dist/assets/pkl-ot-7Btpt.js +1 -0
  198. package/dist/assets/plastic-DQwYfKfQ.js +1 -0
  199. package/dist/assets/plsql-DGHpHOYJ.js +1 -0
  200. package/dist/assets/po-BiJDBrnU.js +1 -0
  201. package/dist/assets/poimandres-DRFjx7u4.js +1 -0
  202. package/dist/assets/polar-C7UOKdEL.js +1 -0
  203. package/dist/assets/postcss-BXeXVLqQ.js +1 -0
  204. package/dist/assets/powerquery-DNMTfnFr.js +1 -0
  205. package/dist/assets/powershell-DshXNtvi.js +1 -0
  206. package/dist/assets/prisma-BsRQq5mF.js +1 -0
  207. package/dist/assets/prolog-iXnhIJG7.js +1 -0
  208. package/dist/assets/proto-DB4EqR-F.js +1 -0
  209. package/dist/assets/pug-DgFsRPbA.js +1 -0
  210. package/dist/assets/puppet-CDv2pdJW.js +1 -0
  211. package/dist/assets/purescript-9MfHhQsQ.js +1 -0
  212. package/dist/assets/python-gzcpVVnB.js +1 -0
  213. package/dist/assets/qml-C_LfdLXm.js +1 -0
  214. package/dist/assets/qmldir-DCQb3MpD.js +1 -0
  215. package/dist/assets/qss-Fe1Jh2GI.js +1 -0
  216. package/dist/assets/r-Pj2SwcAG.js +1 -0
  217. package/dist/assets/racket-DcIDlBhZ.js +1 -0
  218. package/dist/assets/raku-B3gFvitq.js +1 -0
  219. package/dist/assets/razor-BKc5ThWc.js +1 -0
  220. package/dist/assets/red-CJ3rzSJv.js +1 -0
  221. package/dist/assets/reg-CRGYupPL.js +1 -0
  222. package/dist/assets/regexp-BHCMzRa4.js +1 -0
  223. package/dist/assets/rel-BtDbiS_P.js +1 -0
  224. package/dist/assets/riscv-Ckw8ddFX.js +1 -0
  225. package/dist/assets/ron-VUp2lXgN.js +1 -0
  226. package/dist/assets/rose-pine-BthvhNj6.js +1 -0
  227. package/dist/assets/rose-pine-dawn-Dg85fqjY.js +1 -0
  228. package/dist/assets/rose-pine-moon-hon4tzzS.js +1 -0
  229. package/dist/assets/rosmsg-CAekHB0j.js +1 -0
  230. package/dist/assets/rst-D1Q1H8XX.js +1 -0
  231. package/dist/assets/ruby-aFDqyHwf.js +1 -0
  232. package/dist/assets/rust-Cfkwpbl8.js +1 -0
  233. package/dist/assets/sas-DUQasUvv.js +1 -0
  234. package/dist/assets/sass-DXrisJhu.js +1 -0
  235. package/dist/assets/scala-DKOlJaKm.js +1 -0
  236. package/dist/assets/scheme-DQCgrYNe.js +1 -0
  237. package/dist/assets/scss-DRMVx3p6.js +1 -0
  238. package/dist/assets/sdbl-bTVj8UrX.js +1 -0
  239. package/dist/assets/shaderlab-TOUzSsQk.js +1 -0
  240. package/dist/assets/shellscript-9nE2ns3B.js +1 -0
  241. package/dist/assets/shellsession-Ym61uyCB.js +1 -0
  242. package/dist/assets/slack-dark-DnToyrRv.js +1 -0
  243. package/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
  244. package/dist/assets/smalltalk-B16xEiuN.js +1 -0
  245. package/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
  246. package/dist/assets/solarized-dark-DV17i1UV.js +1 -0
  247. package/dist/assets/solarized-light-DSh2HLQt.js +1 -0
  248. package/dist/assets/solidity-CKzVLygQ.js +1 -0
  249. package/dist/assets/soy-C9F5-hVX.js +1 -0
  250. package/dist/assets/sparql-D_iOobhT.js +1 -0
  251. package/dist/assets/splunk-BC2Px7Mm.js +1 -0
  252. package/dist/assets/sql-DNRGB4_D.js +1 -0
  253. package/dist/assets/ssh-config-BgfXC-Er.js +1 -0
  254. package/dist/assets/stata-9CUIUM5m.js +1 -0
  255. package/dist/assets/stylus-B6D30XZt.js +1 -0
  256. package/dist/assets/surrealql-BKa1jBv8.js +1 -0
  257. package/dist/assets/svelte-l-AzXjIs.js +1 -0
  258. package/dist/assets/swift-DonLKvLd.js +1 -0
  259. package/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
  260. package/dist/assets/system-verilog-DJ5XKQeo.js +1 -0
  261. package/dist/assets/systemd-BxMlprV5.js +1 -0
  262. package/dist/assets/talonscript-CohzipZa.js +1 -0
  263. package/dist/assets/tasl-DMoTqEGO.js +1 -0
  264. package/dist/assets/tcl-CZd0xW_V.js +1 -0
  265. package/dist/assets/templ-CcqG4x7p.js +1 -0
  266. package/dist/assets/terraform-DswuEJGm.js +1 -0
  267. package/dist/assets/tex-DqjZxgcw.js +1 -0
  268. package/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
  269. package/dist/assets/toml-CcmNWLt0.js +1 -0
  270. package/dist/assets/ts-tags-7N5waLHg.js +1 -0
  271. package/dist/assets/tsv-sltzmVWM.js +1 -0
  272. package/dist/assets/tsx-CZDiJHa-.js +1 -0
  273. package/dist/assets/turtle-ByJddavk.js +1 -0
  274. package/dist/assets/twig-DnrUvKgH.js +1 -0
  275. package/dist/assets/typescript-CNLx8Xjf.js +1 -0
  276. package/dist/assets/typespec-BRdr0IET.js +1 -0
  277. package/dist/assets/typst-DI99ib-x.js +1 -0
  278. package/dist/assets/v-DETTlOr0.js +1 -0
  279. package/dist/assets/vala-zf12oZj6.js +1 -0
  280. package/dist/assets/vb-Djn5o6TS.js +1 -0
  281. package/dist/assets/verilog-CiiDBU1e.js +1 -0
  282. package/dist/assets/vesper-DdrHHSXu.js +1 -0
  283. package/dist/assets/vhdl-BroJfC0k.js +1 -0
  284. package/dist/assets/viml-DvXPmvsu.js +1 -0
  285. package/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
  286. package/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
  287. package/dist/assets/vitesse-light-VbXTXTou.js +1 -0
  288. package/dist/assets/vue-St5TuCBW.js +1 -0
  289. package/dist/assets/vue-html-BQ_bILAj.js +1 -0
  290. package/dist/assets/vue-vine-BbKxMI1X.js +1 -0
  291. package/dist/assets/vyper-CgoNMtux.js +1 -0
  292. package/dist/assets/wasm-BnjxR4X6.js +1 -0
  293. package/dist/assets/wasm-ByWQv1Qj.js +1 -0
  294. package/dist/assets/wenyan-C8pVoKbM.js +1 -0
  295. package/dist/assets/wgsl-BsKzXJz4.js +1 -0
  296. package/dist/assets/wikitext-ClFFjSW2.js +1 -0
  297. package/dist/assets/wit-DdvCle-K.js +1 -0
  298. package/dist/assets/wolfram-DLL8P-h_.js +1 -0
  299. package/dist/assets/xml-Bz0xg06z.js +1 -0
  300. package/dist/assets/xsl-DC-2vw27.js +1 -0
  301. package/dist/assets/yaml-m0kezuu_.js +1 -0
  302. package/dist/assets/zenscript-BnlCZFoB.js +1 -0
  303. package/dist/assets/zig-CMLA9XwU.js +1 -0
  304. package/dist/icons.svg +24 -24
  305. package/dist/index.html +29 -13
  306. package/dist/llm-full.txt +298 -0
  307. package/dist/llm.txt +110 -0
  308. package/dist/ui-cli.cjs +1 -1
  309. package/package.json +11 -5
  310. package/registry.json +195 -117
  311. package/scripts/build-cli.mjs +12 -12
  312. package/scripts/build-registry.ts +261 -261
  313. package/scripts/ui-cli.ts +1333 -1333
  314. package/dist/assets/index-1YAQdTE0.css +0 -2
  315. package/dist/assets/index-BsQ6nn74.js +0 -237
  316. package/dist/ui-cli.js +0 -124
package/scripts/ui-cli.ts CHANGED
@@ -1,1333 +1,1333 @@
1
- #!/usr/bin/env node
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { execSync } from 'child_process';
5
- import readline from 'readline';
6
-
7
- // ─── Constants ────────────────────────────────────────────────────────────────
8
-
9
- const VERSION = '0.3.11';
10
- const REGISTRY_LOCAL = './registry.json';
11
- const REGISTRY_REMOTE = 'https://raw.githubusercontent.com/Basuicn/basuicn-core/main/registry.json';
12
-
13
- // ─── Colors (ANSI) ───────────────────────────────────────────────────────────
14
-
15
- const c = {
16
- reset: '\x1b[0m',
17
- bold: '\x1b[1m',
18
- dim: '\x1b[2m',
19
- green: '\x1b[32m',
20
- yellow: '\x1b[33m',
21
- red: '\x1b[31m',
22
- cyan: '\x1b[36m',
23
- magenta: '\x1b[35m',
24
- blue: '\x1b[34m',
25
- gray: '\x1b[90m',
26
- };
27
-
28
- const log = (msg: string) => console.log(`${c.cyan}▸${c.reset} ${msg}`);
29
- const ok = (msg: string) => console.log(`${c.green}✔${c.reset} ${msg}`);
30
- const warn = (msg: string) => console.warn(`${c.yellow}⚠${c.reset} ${msg}`);
31
- const error = (msg: string) => console.error(`${c.red}✖${c.reset} ${msg}`);
32
-
33
- const getTargetProjectDir = () => process.cwd();
34
-
35
- // ─── Framework detection ──────────────────────────────────────────────────────
36
-
37
- type Framework = 'vite' | 'nextjs-app' | 'nextjs-pages';
38
-
39
- const detectFramework = (cwd: string): Framework => {
40
- const hasNextConfig =
41
- fs.existsSync(path.join(cwd, 'next.config.js')) ||
42
- fs.existsSync(path.join(cwd, 'next.config.ts')) ||
43
- fs.existsSync(path.join(cwd, 'next.config.mjs'));
44
-
45
- const hasNextDep = (() => {
46
- const pkgPath = path.join(cwd, 'package.json');
47
- if (!fs.existsSync(pkgPath)) return false;
48
- try {
49
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as Record<string, unknown>;
50
- const all = { ...(pkg.dependencies as object || {}), ...(pkg.devDependencies as object || {}) } as Record<string, string>;
51
- return !!all['next'];
52
- } catch { return false; }
53
- })();
54
-
55
- if (hasNextConfig || hasNextDep) {
56
- const hasAppDir =
57
- fs.existsSync(path.join(cwd, 'src/app')) ||
58
- fs.existsSync(path.join(cwd, 'app'));
59
- const hasPagesDir =
60
- fs.existsSync(path.join(cwd, 'src/pages')) ||
61
- fs.existsSync(path.join(cwd, 'pages'));
62
- if (hasPagesDir && !hasAppDir) return 'nextjs-pages';
63
- return 'nextjs-app';
64
- }
65
-
66
- return 'vite';
67
- };
68
-
69
- // ─── Interactive prompt ──────────────────────────────────────────────────────
70
-
71
- const ask = (question: string): Promise<string> => {
72
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
73
- return new Promise((resolve) => {
74
- rl.question(`${c.cyan}?${c.reset} ${question} `, (answer) => {
75
- rl.close();
76
- resolve(answer.trim());
77
- });
78
- });
79
- };
80
-
81
- const confirm = async (question: string, defaultYes = true): Promise<boolean> => {
82
- const hint = defaultYes ? 'Y/n' : 'y/N';
83
- const answer = await ask(`${question} ${c.dim}(${hint})${c.reset}`);
84
- if (!answer) return defaultYes;
85
- return answer.toLowerCase().startsWith('y');
86
- };
87
-
88
- // ─── Registry ─────────────────────────────────────────────────────────────────
89
-
90
- interface RegistryFile { path: string; content: string }
91
- interface RegistryComponent {
92
- dependencies: string[];
93
- internalDependencies?: string[];
94
- files: RegistryFile[];
95
- }
96
- interface Registry {
97
- core?: { dependencies: string[]; files: RegistryFile[] };
98
- components: Record<string, RegistryComponent>;
99
- }
100
-
101
- const validateRegistry = (data: unknown): data is Registry => {
102
- if (!data || typeof data !== 'object') return false;
103
- const reg = data as Record<string, unknown>;
104
- return 'components' in reg && typeof reg.components === 'object' && reg.components !== null;
105
- };
106
-
107
- const getRegistry = async (isLocal: boolean): Promise<Registry> => {
108
- if (isLocal && fs.existsSync(REGISTRY_LOCAL)) {
109
- log('Using local registry...');
110
- try {
111
- const data = JSON.parse(fs.readFileSync(REGISTRY_LOCAL, 'utf-8'));
112
- if (!validateRegistry(data)) {
113
- error('Invalid local registry format — missing "components" field.');
114
- process.exit(1);
115
- }
116
- return data;
117
- } catch (err) {
118
- error(`Failed to parse local registry: ${err instanceof Error ? err.message : err}`);
119
- process.exit(1);
120
- }
121
- }
122
-
123
- log('Fetching registry from remote...');
124
- try {
125
- const response = await fetch(REGISTRY_REMOTE);
126
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
127
- const data = await response.json();
128
- if (!validateRegistry(data)) {
129
- error('Invalid remote registry format — missing "components" field.');
130
- process.exit(1);
131
- }
132
- return data;
133
- } catch (err: unknown) {
134
- const message = err instanceof Error ? err.message : String(err);
135
- error(`Cannot fetch registry: ${message}`);
136
- process.exit(1);
137
- }
138
- };
139
-
140
- // ─── npm ──────────────────────────────────────────────────────────────────────
141
-
142
- const installNpmPackages = (packages: string[], cwd: string, dev = false) => {
143
- if (packages.length === 0) return;
144
-
145
- const pkgJsonPath = path.join(cwd, 'package.json');
146
- let toInstall = packages;
147
-
148
- if (fs.existsSync(pkgJsonPath)) {
149
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
150
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
151
- toInstall = packages.filter((p) => !allDeps[p]);
152
- }
153
-
154
- if (toInstall.length === 0) return;
155
-
156
- log(`Installing: ${c.bold}${toInstall.join(', ')}${c.reset}...`);
157
- const flag = dev ? '--save-dev' : '--save';
158
- try {
159
- execSync(`npm install ${toInstall.join(' ')} ${flag}`, { stdio: 'inherit', cwd });
160
- } catch (err) {
161
- error(`Failed to install packages: ${toInstall.join(', ')}. ${err instanceof Error ? err.message : ''}`);
162
- }
163
- };
164
-
165
- // ─── Packages ─────────────────────────────────────────────────────────────────
166
-
167
- const VITE_DEV_PACKAGES = [
168
- 'tailwindcss',
169
- '@tailwindcss/vite',
170
- '@vitejs/plugin-react',
171
- '@types/node',
172
- ];
173
-
174
- const RUNTIME_PACKAGES = [
175
- '@base-ui/react',
176
- 'tailwind-variants',
177
- 'clsx',
178
- 'tailwind-merge',
179
- 'tailwindcss-animate',
180
- 'lucide-react',
181
- ];
182
-
183
- const NEXTJS_DEV_PACKAGES = [
184
- 'tailwindcss',
185
- '@tailwindcss/postcss',
186
- '@types/node',
187
- ];
188
-
189
- const POSTCSS_CONFIG_TEMPLATE = `const config = {
190
- plugins: {
191
- '@tailwindcss/postcss': {},
192
- },
193
- };
194
-
195
- export default config;
196
- `;
197
-
198
- // ─── Vite config ──────────────────────────────────────────────────────────────
199
-
200
- const VITE_CONFIG_TEMPLATE = `import { defineConfig } from 'vite';
201
- import tailwindcss from '@tailwindcss/vite';
202
- import react from '@vitejs/plugin-react';
203
- import path from 'path';
204
-
205
- export default defineConfig({
206
- plugins: [tailwindcss(), react()],
207
- resolve: {
208
- alias: {
209
- '@': path.resolve(__dirname, './src'),
210
- '@lib': path.resolve(__dirname, './src/lib'),
211
- '@components': path.resolve(__dirname, './src/components'),
212
- '@assets': path.resolve(__dirname, './src/assets'),
213
- '@pages': path.resolve(__dirname, './src/pages'),
214
- '@styles': path.resolve(__dirname, './src/styles'),
215
- },
216
- },
217
- });
218
- `;
219
-
220
- const TSCONFIG_PATHS = {
221
- '@/*': ['./src/*'],
222
- '@lib/*': ['./src/lib/*'],
223
- '@components/*': ['./src/components/*'],
224
- '@assets/*': ['./src/assets/*'],
225
- '@pages/*': ['./src/pages/*'],
226
- '@styles/*': ['./src/styles/*'],
227
- };
228
-
229
- const setupViteConfig = (cwd: string) => {
230
- installNpmPackages(VITE_DEV_PACKAGES, cwd, true);
231
-
232
- const configTs = path.join(cwd, 'vite.config.ts');
233
- const configJs = path.join(cwd, 'vite.config.js');
234
-
235
- if (!fs.existsSync(configTs) && !fs.existsSync(configJs)) {
236
- fs.writeFileSync(configTs, VITE_CONFIG_TEMPLATE);
237
- ok('Created vite.config.ts.');
238
- return;
239
- }
240
-
241
- const existingPath = fs.existsSync(configTs) ? configTs : configJs;
242
- let content = fs.readFileSync(existingPath, 'utf-8');
243
-
244
- const missingImports: string[] = [];
245
- if (!content.includes('@tailwindcss/vite')) missingImports.push("import tailwindcss from '@tailwindcss/vite';");
246
- if (!content.includes('@vitejs/plugin-react')) missingImports.push("import react from '@vitejs/plugin-react';");
247
- if (!content.includes("from 'path'") && !content.includes('from "path"')) missingImports.push("import path from 'path';");
248
-
249
- const missingPlugins: string[] = [];
250
- if (!content.includes('tailwindcss()')) missingPlugins.push('tailwindcss()');
251
- if (!content.includes('react()') && !content.includes('react({')) missingPlugins.push('react()');
252
-
253
- const hasAlias = content.includes('alias:') || content.includes("'@'") || content.includes('"@"');
254
-
255
- if (missingImports.length === 0 && missingPlugins.length === 0 && hasAlias) {
256
- ok('vite.config already configured — skipping.');
257
- return;
258
- }
259
-
260
- if (missingImports.length > 0) {
261
- const importBlock = missingImports.join('\n');
262
- const allImports = [...content.matchAll(/^import\s.+$/gm)];
263
- if (allImports.length > 0) {
264
- const last = allImports[allImports.length - 1];
265
- const pos = last.index! + last[0].length;
266
- content = content.slice(0, pos) + '\n' + importBlock + content.slice(pos);
267
- } else {
268
- content = importBlock + '\n' + content;
269
- }
270
- }
271
-
272
- if (missingPlugins.length > 0) {
273
- const match = content.match(/plugins:\s*\[/);
274
- if (match && match.index !== undefined) {
275
- const pos = match.index + match[0].length;
276
- const after = content.slice(pos);
277
- const pluginLines = missingPlugins.map((p) => `\n ${p},`).join('');
278
- const needsNewline = after.length > 0 && after[0] !== '\n' && after[0] !== '\r';
279
- content = content.slice(0, pos) + pluginLines + (needsNewline ? '\n ' : '') + after;
280
- }
281
- }
282
-
283
- if (!hasAlias) {
284
- const aliasBlock = [
285
- ' resolve: {',
286
- ' alias: {',
287
- " '@': path.resolve(__dirname, './src'),",
288
- " '@lib': path.resolve(__dirname, './src/lib'),",
289
- " '@components': path.resolve(__dirname, './src/components'),",
290
- " '@assets': path.resolve(__dirname, './src/assets'),",
291
- " '@pages': path.resolve(__dirname, './src/pages'),",
292
- " '@styles': path.resolve(__dirname, './src/styles'),",
293
- ' },',
294
- ' },',
295
- ].join('\n');
296
-
297
- const pluginsStart = content.search(/plugins:\s*\[/);
298
- if (pluginsStart !== -1) {
299
- let depth = 0;
300
- let foundStart = false;
301
- for (let i = pluginsStart; i < content.length; i++) {
302
- if (content[i] === '[') { depth++; foundStart = true; }
303
- if (content[i] === ']') depth--;
304
- if (foundStart && depth === 0) {
305
- let lineEnd = content.indexOf('\n', i);
306
- if (lineEnd === -1) lineEnd = content.length;
307
- content = content.slice(0, lineEnd + 1) + aliasBlock + '\n' + content.slice(lineEnd + 1);
308
- break;
309
- }
310
- }
311
- }
312
- }
313
-
314
- fs.writeFileSync(existingPath, content);
315
- ok(`Updated ${path.basename(existingPath)} with Tailwind + path aliases.`);
316
- };
317
-
318
- // ─── Next.js config ───────────────────────────────────────────────────────────
319
-
320
- const setupNextConfig = (cwd: string) => {
321
- installNpmPackages(NEXTJS_DEV_PACKAGES, cwd, true);
322
-
323
- const postcssCandidates = ['postcss.config.mjs', 'postcss.config.js', 'postcss.config.cjs'];
324
- const existingPostcss = postcssCandidates.map(f => path.join(cwd, f)).find(p => fs.existsSync(p));
325
-
326
- if (!existingPostcss) {
327
- fs.writeFileSync(path.join(cwd, 'postcss.config.mjs'), POSTCSS_CONFIG_TEMPLATE);
328
- ok('Created postcss.config.mjs with @tailwindcss/postcss.');
329
- return;
330
- }
331
-
332
- const content = fs.readFileSync(existingPostcss, 'utf-8');
333
- if (!content.includes('@tailwindcss/postcss') && !content.includes('tailwindcss')) {
334
- warn(`${path.basename(existingPostcss)} found but missing Tailwind plugin — add '@tailwindcss/postcss': {} to plugins manually.`);
335
- } else {
336
- ok(`${path.basename(existingPostcss)} already configured — skipping.`);
337
- }
338
- };
339
-
340
- // ─── tsconfig ─────────────────────────────────────────────────────────────────
341
-
342
- const setupTsConfig = (cwd: string) => {
343
- const candidates = ['tsconfig.app.json', 'tsconfig.json'];
344
-
345
- for (const candidate of candidates) {
346
- const configPath = path.join(cwd, candidate);
347
- if (!fs.existsSync(configPath)) continue;
348
-
349
- const raw = fs.readFileSync(configPath, 'utf-8');
350
-
351
- if (raw.includes('"@/*"') || raw.includes("'@/*'")) {
352
- ok(`${candidate} already has path aliases — skipping.`);
353
- return;
354
- }
355
-
356
- try {
357
- const stripped = raw
358
- .replace(/\/\*[\s\S]*?\*\//g, '')
359
- .replace(/(^|[\s,{[\]])\/\/[^\n]*/g, '$1');
360
- const parsed = JSON.parse(stripped) as { compilerOptions?: Record<string, unknown> };
361
- if (!parsed.compilerOptions) parsed.compilerOptions = {};
362
- parsed.compilerOptions.baseUrl = '.';
363
- parsed.compilerOptions.paths = TSCONFIG_PATHS;
364
- fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2));
365
- ok(`Added path aliases to ${candidate}.`);
366
- } catch (err) {
367
- warn(`Could not auto-patch ${candidate}: ${err instanceof Error ? err.message : err}`);
368
- warn('Add these to compilerOptions manually:');
369
- console.log('\n "baseUrl": ".",');
370
- console.log(' "paths": {');
371
- for (const [alias, targets] of Object.entries(TSCONFIG_PATHS)) {
372
- console.log(` "${alias}": ["${targets[0]}"],`);
373
- }
374
- console.log(' }');
375
- console.log('');
376
- }
377
- return;
378
- }
379
-
380
- const newConfig = { compilerOptions: { baseUrl: '.', paths: TSCONFIG_PATHS } };
381
- fs.writeFileSync(path.join(cwd, 'tsconfig.json'), JSON.stringify(newConfig, null, 2));
382
- ok('Created tsconfig.json with path aliases.');
383
- };
384
-
385
- // ─── Core files ───────────────────────────────────────────────────────────────
386
-
387
- const ensureCore = (
388
- registry: { core?: { dependencies: string[]; files: RegistryFile[] } },
389
- cwd: string,
390
- options: { force?: boolean } = {}
391
- ) => {
392
- const core = registry.core;
393
- if (!core) return;
394
-
395
- installNpmPackages(core.dependencies, cwd);
396
-
397
- for (const file of core.files) {
398
- const targetPath = path.join(cwd, file.path);
399
- const targetDir = path.dirname(targetPath);
400
-
401
- if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
402
-
403
- if (fs.existsSync(targetPath) && !options.force) {
404
- log(`Core file exists (skipping): ${c.dim}${file.path}${c.reset}`);
405
- continue;
406
- }
407
-
408
- fs.writeFileSync(targetPath, file.content);
409
- ok(`${fs.existsSync(targetPath) ? 'Updated' : 'Created'} core file: ${file.path}`);
410
- }
411
- };
412
-
413
- // ─── Next.js layout patching ─────────────────────────────────────────────────
414
-
415
- const NEXT_APP_LAYOUT_CANDIDATES = [
416
- 'src/app/layout.tsx', 'src/app/layout.jsx',
417
- 'app/layout.tsx', 'app/layout.jsx',
418
- ];
419
-
420
- const NEXT_PAGES_APP_CANDIDATES = [
421
- 'src/pages/_app.tsx', 'src/pages/_app.jsx',
422
- 'pages/_app.tsx', 'pages/_app.jsx',
423
- ];
424
-
425
- const findNextLayoutFile = (cwd: string): string | null => {
426
- for (const c of NEXT_APP_LAYOUT_CANDIDATES) {
427
- const p = path.join(cwd, c);
428
- if (fs.existsSync(p)) return p;
429
- }
430
- return null;
431
- };
432
-
433
- const findNextPagesAppFile = (cwd: string): string | null => {
434
- for (const c of NEXT_PAGES_APP_CANDIDATES) {
435
- const p = path.join(cwd, c);
436
- if (fs.existsSync(p)) return p;
437
- }
438
- return null;
439
- };
440
-
441
- const patchNextLayout = (cwd: string) => {
442
- const layoutPath = findNextLayoutFile(cwd);
443
- if (!layoutPath) {
444
- warn('Could not find app/layout.tsx — add ThemeProvider and CSS import manually.');
445
- return;
446
- }
447
-
448
- let content = fs.readFileSync(layoutPath, 'utf-8');
449
- let changed = false;
450
-
451
- // CSS import
452
- const cssImport = "import '@/styles/index.css';";
453
- if (!content.includes('styles/index.css') && !content.includes('index.css')) {
454
- const firstImport = content.match(/^import\s/m);
455
- if (firstImport?.index !== undefined) {
456
- content = content.slice(0, firstImport.index) + cssImport + '\n' + content.slice(firstImport.index);
457
- } else {
458
- content = cssImport + '\n' + content;
459
- }
460
- changed = true;
461
- }
462
-
463
- // ThemeProvider
464
- if (!content.includes('ThemeProvider')) {
465
- content = insertImport(content, "import { ThemeProvider } from '@/lib/theme/ThemeProvider';");
466
- // Wrap {children} with ThemeProvider
467
- const wrapped = content.replace(/\{children\}/, '<ThemeProvider>{children}</ThemeProvider>');
468
- if (wrapped !== content) {
469
- content = wrapped;
470
- } else {
471
- warn('Could not locate {children} in layout.tsx — add <ThemeProvider> wrapper manually.');
472
- }
473
- changed = true;
474
- }
475
-
476
- if (changed) {
477
- fs.writeFileSync(layoutPath, content);
478
- ok(`Patched ${path.relative(cwd, layoutPath)}.`);
479
- } else {
480
- ok(`${path.relative(cwd, layoutPath)} already configured — skipping.`);
481
- }
482
- };
483
-
484
- const patchNextPagesApp = (cwd: string) => {
485
- const appPath = findNextPagesAppFile(cwd);
486
- if (!appPath) {
487
- warn('Could not find pages/_app.tsx — add ThemeProvider and CSS import manually.');
488
- return;
489
- }
490
-
491
- let content = fs.readFileSync(appPath, 'utf-8');
492
- let changed = false;
493
-
494
- // CSS import
495
- const cssImport = "import '@/styles/index.css';";
496
- if (!content.includes('styles/index.css') && !content.includes('index.css')) {
497
- content = insertImport(content, cssImport);
498
- changed = true;
499
- }
500
-
501
- // ThemeProvider
502
- if (!content.includes('ThemeProvider')) {
503
- content = insertImport(content, "import { ThemeProvider } from '@/lib/theme/ThemeProvider';");
504
- const wrapped = content.replace(/(<Component\s[^/]*\/\s*>)/, '<ThemeProvider>\n $1\n </ThemeProvider>');
505
- if (wrapped !== content) {
506
- content = wrapped;
507
- } else {
508
- warn('Could not locate <Component .../> in _app.tsx — add <ThemeProvider> wrapper manually.');
509
- }
510
- changed = true;
511
- }
512
-
513
- if (changed) {
514
- fs.writeFileSync(appPath, content);
515
- ok(`Patched ${path.relative(cwd, appPath)}.`);
516
- } else {
517
- ok(`${path.relative(cwd, appPath)} already configured — skipping.`);
518
- }
519
- };
520
-
521
- const patchNextLayoutComponent = (cwd: string, patch: { import: string; jsx: string }) => {
522
- const layoutPath = findNextLayoutFile(cwd);
523
- if (!layoutPath) {
524
- warn(`Could not find Next.js layout — add ${patch.jsx} manually.`);
525
- return;
526
- }
527
-
528
- let content = fs.readFileSync(layoutPath, 'utf-8');
529
- const tagName = patch.jsx.match(/<(\w+)/)?.[1];
530
- if (tagName && content.includes(`<${tagName}`)) return;
531
-
532
- content = insertImport(content, patch.import);
533
- // Insert before </body>
534
- const updated = content.replace(/<\/body>/, ` ${patch.jsx}\n </body>`);
535
- if (updated !== content) {
536
- fs.writeFileSync(layoutPath, updated);
537
- ok(`Added <${tagName}> to ${path.relative(cwd, layoutPath)}.`);
538
- } else {
539
- warn(`Could not auto-add <${tagName}> to layout.tsx — add it manually.`);
540
- }
541
- };
542
-
543
- const patchNextPagesAppComponent = (cwd: string, patch: { import: string; jsx: string }) => {
544
- const appPath = findNextPagesAppFile(cwd);
545
- if (!appPath) {
546
- warn(`Could not find pages/_app.tsx — add ${patch.jsx} manually.`);
547
- return;
548
- }
549
-
550
- let content = fs.readFileSync(appPath, 'utf-8');
551
- const tagName = patch.jsx.match(/<(\w+)/)?.[1];
552
- if (tagName && content.includes(`<${tagName}`)) return;
553
-
554
- content = insertImport(content, patch.import);
555
- // Insert after <Component .../>
556
- let updated = content.replace(
557
- /(<Component\s[^/]*\/\s*>)(\s*\n\s*<\/ThemeProvider>)/,
558
- `$1\n ${patch.jsx}$2`
559
- );
560
- if (updated === content) {
561
- updated = content.replace(/(<Component\s[^/]*\/\s*>)/, `$1\n ${patch.jsx}`);
562
- }
563
- if (updated !== content) {
564
- fs.writeFileSync(appPath, updated);
565
- ok(`Added <${tagName}> to ${path.relative(cwd, appPath)}.`);
566
- } else {
567
- warn(`Could not auto-add <${tagName}> to _app.tsx — add it manually.`);
568
- }
569
- };
570
-
571
- const patchEntryFile = (cwd: string, framework: Framework) => {
572
- if (framework === 'nextjs-app') patchNextLayout(cwd);
573
- else if (framework === 'nextjs-pages') patchNextPagesApp(cwd);
574
- else patchMainTsx(cwd);
575
- };
576
-
577
- const patchEntryComponentFile = (cwd: string, componentName: string, framework: Framework) => {
578
- const patch = MAIN_PATCH_COMPONENTS[componentName];
579
- if (!patch) return;
580
- if (framework === 'nextjs-app') patchNextLayoutComponent(cwd, patch);
581
- else if (framework === 'nextjs-pages') patchNextPagesAppComponent(cwd, patch);
582
- else patchMainTsxComponent(cwd, componentName);
583
- };
584
-
585
- // ─── main.tsx patching ────────────────────────────────────────────────────────
586
-
587
- const MAIN_PATCH_COMPONENTS: Record<string, { import: string; jsx: string }> = {
588
- toast: {
589
- import: "import { Toaster } from '@/components/ui/toast/Toaster';",
590
- jsx: '<Toaster position="top-center" expand={true} richColors />',
591
- },
592
- };
593
-
594
- const MAIN_CANDIDATES = ['src/main.tsx', 'src/main.jsx', 'src/index.tsx', 'src/index.jsx'];
595
-
596
- const findMainFile = (cwd: string): string | null => {
597
- for (const c of MAIN_CANDIDATES) {
598
- const p = path.join(cwd, c);
599
- if (fs.existsSync(p)) return p;
600
- }
601
- return null;
602
- };
603
-
604
- const insertImport = (content: string, importLine: string): string => {
605
- if (content.includes(importLine)) return content;
606
- const allImports = [...content.matchAll(/^import\s.+$/gm)];
607
- if (allImports.length > 0) {
608
- const last = allImports[allImports.length - 1];
609
- const pos = last.index! + last[0].length;
610
- return content.slice(0, pos) + '\n' + importLine + content.slice(pos);
611
- }
612
- return importLine + '\n' + content;
613
- };
614
-
615
- const patchMainTsx = (cwd: string) => {
616
- const mainPath = findMainFile(cwd);
617
- if (!mainPath) {
618
- warn('Could not find entry file (src/main.tsx). Skipping main entry setup.');
619
- return;
620
- }
621
-
622
- let content = fs.readFileSync(mainPath, 'utf-8');
623
- let changed = false;
624
-
625
- const cssImportLine = "import './styles/index.css';";
626
- const hasCssImport = content.includes('styles/index.css') || content.includes('index.css');
627
- if (!hasCssImport) {
628
- const firstImport = content.match(/^import\s/m);
629
- if (firstImport?.index !== undefined) {
630
- content = content.slice(0, firstImport.index) + cssImportLine + '\n' + content.slice(firstImport.index);
631
- } else {
632
- content = cssImportLine + '\n' + content;
633
- }
634
- changed = true;
635
- } else if (!content.includes('styles/index.css')) {
636
- content = insertImport(content, cssImportLine);
637
- changed = true;
638
- }
639
-
640
- if (!content.includes('ThemeProvider')) {
641
- content = insertImport(content, "import { ThemeProvider } from '@/lib/theme/ThemeProvider';");
642
-
643
- const wrapped = content.replace(/(<App\s*\/>)/g, '<ThemeProvider>\n $1\n </ThemeProvider>');
644
- if (wrapped === content) {
645
- warn('Could not locate <App /> in entry file — add <ThemeProvider> wrapper manually.');
646
- } else {
647
- content = wrapped;
648
- }
649
- changed = true;
650
- }
651
-
652
- if (changed) {
653
- fs.writeFileSync(mainPath, content);
654
- ok(`Patched ${path.relative(cwd, mainPath)}.`);
655
- } else {
656
- ok(`${path.relative(cwd, mainPath)} already configured — skipping.`);
657
- }
658
- };
659
-
660
- const patchMainTsxComponent = (cwd: string, componentName: string) => {
661
- const patch = MAIN_PATCH_COMPONENTS[componentName];
662
- if (!patch) return;
663
-
664
- const mainPath = findMainFile(cwd);
665
- if (!mainPath) return;
666
-
667
- let content = fs.readFileSync(mainPath, 'utf-8');
668
- const tagName = patch.jsx.match(/<(\w+)/)?.[1];
669
- if (tagName && content.includes(`<${tagName}`)) return;
670
-
671
- content = insertImport(content, patch.import);
672
-
673
- const withProvider = content.replace(
674
- /(<App\s*\/>)(\s*\n\s*<\/ThemeProvider>)/,
675
- `$1\n ${patch.jsx}$2`
676
- );
677
-
678
- if (withProvider !== content) {
679
- fs.writeFileSync(mainPath, withProvider);
680
- } else {
681
- const fallback = content.replace(/(<App\s*\/>)/, `$1\n ${patch.jsx}`);
682
- if (fallback !== content) fs.writeFileSync(mainPath, fallback);
683
- }
684
-
685
- ok(`Added <${tagName}> to ${path.relative(cwd, mainPath)}.`);
686
- };
687
-
688
- // ─── index.ts barrel update ───────────────────────────────────────────────────
689
-
690
- const UI_INDEX_PATH = 'src/components/ui/index.ts';
691
- const UI_INDEX_DIR = 'src/components/ui';
692
-
693
- /** Pick the primary .tsx component file (skip tests, stories, hooks) */
694
- const pickMainFile = (files: RegistryFile[]): RegistryFile | undefined =>
695
- files.find(
696
- (f) =>
697
- f.path.startsWith(UI_INDEX_DIR + '/') &&
698
- f.path.endsWith('.tsx') &&
699
- !f.path.includes('.test.') &&
700
- !f.path.includes('.stories.'),
701
- );
702
-
703
- /** Append `export * from './dir/File';` to index.ts if not already present */
704
- const addToComponentIndex = (componentFiles: RegistryFile[], cwd: string) => {
705
- const indexPath = path.join(cwd, UI_INDEX_PATH);
706
- if (!fs.existsSync(indexPath)) return;
707
-
708
- const mainFile = pickMainFile(componentFiles);
709
- if (!mainFile) return;
710
-
711
- const withoutExt = mainFile.path.replace(/\.tsx$/, '');
712
- const relPath = './' + path.relative(UI_INDEX_DIR, withoutExt).replace(/\\/g, '/');
713
- const exportLine = `export * from '${relPath}';`;
714
-
715
- const content = fs.readFileSync(indexPath, 'utf-8');
716
- if (content.includes(relPath)) return;
717
-
718
- fs.writeFileSync(indexPath, content.trimEnd() + '\n' + exportLine + '\n');
719
- ok(`Updated index.ts: added ${c.dim}${relPath}${c.reset}`);
720
- };
721
-
722
- /** Remove the export line from index.ts */
723
- const removeFromComponentIndex = (componentFiles: RegistryFile[], cwd: string) => {
724
- const indexPath = path.join(cwd, UI_INDEX_PATH);
725
- if (!fs.existsSync(indexPath)) return;
726
-
727
- const mainFile = pickMainFile(componentFiles);
728
- if (!mainFile) return;
729
-
730
- const withoutExt = mainFile.path.replace(/\.tsx$/, '');
731
- const relPath = './' + path.relative(UI_INDEX_DIR, withoutExt).replace(/\\/g, '/');
732
-
733
- const content = fs.readFileSync(indexPath, 'utf-8');
734
- const filtered = content
735
- .split('\n')
736
- .filter((line) => !line.includes(relPath))
737
- .join('\n');
738
-
739
- if (filtered === content) return;
740
- fs.writeFileSync(indexPath, filtered);
741
- ok(`Updated index.ts: removed ${c.dim}${relPath}${c.reset}`);
742
- };
743
-
744
- // ─── Component add/remove ─────────────────────────────────────────────────────
745
-
746
- const addComponent = (
747
- name: string,
748
- registry: { core?: unknown; components: Record<string, RegistryComponent> },
749
- cwd: string,
750
- options: { force: boolean; framework?: Framework },
751
- added: Set<string> = new Set()
752
- ) => {
753
- if (added.has(name)) return;
754
- added.add(name);
755
-
756
- const component = registry.components[name];
757
- if (!component) {
758
- error(`Component "${name}" not found. Run '${c.cyan}basuicn list${c.reset}' to see available components.`);
759
- return;
760
- }
761
-
762
- log(`Adding: ${c.bold}${name}${c.reset}...`);
763
-
764
- ensureCore(registry as Parameters<typeof ensureCore>[0], cwd);
765
- installNpmPackages(component.dependencies, cwd);
766
-
767
- if (component.internalDependencies) {
768
- for (const dep of component.internalDependencies) {
769
- if (registry.components[dep]) {
770
- addComponent(dep, registry, cwd, options, added);
771
- }
772
- }
773
- }
774
-
775
- for (const file of component.files) {
776
- const targetPath = path.join(cwd, file.path);
777
- const targetDir = path.dirname(targetPath);
778
-
779
- if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
780
-
781
- if (fs.existsSync(targetPath) && !options.force) {
782
- warn(`Skipped (exists): ${file.path} — use ${c.cyan}--force${c.reset} to overwrite`);
783
- continue;
784
- }
785
-
786
- // Add 'use client' for Next.js App Router TSX components that don't already have it
787
- let content = file.content;
788
- if (options.framework === 'nextjs-app' && file.path.endsWith('.tsx')) {
789
- if (!content.startsWith("'use client'") && !content.startsWith('"use client"')) {
790
- content = "'use client';\n" + content;
791
- }
792
- }
793
-
794
- fs.writeFileSync(targetPath, content);
795
- ok(`Created: ${file.path}`);
796
- }
797
-
798
- addToComponentIndex(component.files, cwd);
799
- };
800
-
801
- const removeComponent = (
802
- name: string,
803
- registry: { components: Record<string, { files: { path: string }[] }> },
804
- cwd: string
805
- ) => {
806
- const component = registry.components[name];
807
- if (!component) {
808
- error(`Component "${name}" not found.`);
809
- return;
810
- }
811
-
812
- log(`Removing: ${c.bold}${name}${c.reset}...`);
813
-
814
- for (const file of component.files) {
815
- const targetPath = path.join(cwd, file.path);
816
- if (fs.existsSync(targetPath)) {
817
- fs.unlinkSync(targetPath);
818
- ok(`Deleted: ${file.path}`);
819
- }
820
- }
821
-
822
- for (const file of component.files) {
823
- const targetDir = path.dirname(path.join(cwd, file.path));
824
- try {
825
- if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length === 0) {
826
- fs.rmdirSync(targetDir);
827
- ok(`Removed empty dir: ${path.relative(cwd, targetDir)}`);
828
- }
829
- } catch (err) {
830
- warn(`Could not remove directory: ${err instanceof Error ? err.message : err}`);
831
- }
832
- }
833
-
834
- removeFromComponentIndex(component.files as RegistryFile[], cwd);
835
- };
836
-
837
- // ─── Help texts ───────────────────────────────────────────────────────────────
838
-
839
- const HELP_MAIN = `
840
- ${c.bold}${c.cyan}basuicn${c.reset} ${c.dim}v${VERSION}${c.reset} — Modern React UI Component CLI
841
-
842
- ${c.bold}USAGE${c.reset}
843
- ${c.cyan}npx basuicn${c.reset} ${c.green}<command>${c.reset} ${c.dim}[options]${c.reset}
844
-
845
- ${c.bold}COMMANDS${c.reset}
846
- ${c.green}init${c.reset} Initialize project: install deps, copy core files, patch entry
847
- ${c.green}add${c.reset} ${c.dim}<name...>${c.reset} Add component(s) to your project
848
- ${c.green}update${c.reset} ${c.dim}<name...>${c.reset} Update component(s) to latest registry version
849
- ${c.green}diff${c.reset} ${c.dim}<name...>${c.reset} Show diff between local and registry version
850
- ${c.green}remove${c.reset} ${c.dim}<name...>${c.reset} Remove component(s) from your project
851
- ${c.green}list${c.reset} List all available components
852
- ${c.green}doctor${c.reset} Check project health and configuration
853
-
854
- ${c.bold}OPTIONS${c.reset}
855
- ${c.cyan}--force${c.reset} Overwrite existing files when adding/updating
856
- ${c.cyan}--local${c.reset} Use local registry.json instead of remote
857
- ${c.cyan}--help, -h${c.reset} Show help (use with a command for detailed help)
858
- ${c.cyan}--version, -v${c.reset} Show version
859
-
860
- ${c.bold}QUICK START${c.reset}
861
- ${c.dim}$${c.reset} npx basuicn init
862
- ${c.dim}$${c.reset} npx basuicn add button input card
863
- ${c.dim}$${c.reset} npx basuicn add toast
864
-
865
- ${c.bold}EXAMPLES${c.reset}
866
- ${c.dim}$${c.reset} npx basuicn add dialog --force ${c.dim}# Overwrite existing dialog${c.reset}
867
- ${c.dim}$${c.reset} npx basuicn diff button ${c.dim}# See what changed since last update${c.reset}
868
- ${c.dim}$${c.reset} npx basuicn doctor ${c.dim}# Diagnose missing deps/config${c.reset}
869
-
870
- ${c.dim}Documentation: https://github.com/Basuicn/basuicn-core${c.reset}
871
- `;
872
-
873
- const HELP_COMMANDS: Record<string, string> = {
874
- init: `
875
- ${c.bold}basuicn init${c.reset}
876
-
877
- Initialize your project for basuicn components.
878
- Auto-detects Vite or Next.js (App Router / Pages Router).
879
-
880
- ${c.bold}What it does (Vite):${c.reset}
881
- 1. Installs runtime dependencies (@base-ui/react, tailwind-variants, etc.)
882
- 2. Sets up vite.config.ts with Tailwind CSS + path aliases
883
- 3. Patches tsconfig.json with path aliases (@/*, @lib/*, etc.)
884
- 4. Copies core files (cn.ts, themes.ts, ThemeProvider.tsx, index.css)
885
- 5. Wraps your <App /> with <ThemeProvider> in src/main.tsx
886
-
887
- ${c.bold}What it does (Next.js):${c.reset}
888
- 1. Installs runtime dependencies (@base-ui/react, tailwind-variants, etc.)
889
- 2. Sets up postcss.config.mjs with @tailwindcss/postcss
890
- 3. Patches tsconfig.json with path aliases (@/*, @lib/*, etc.)
891
- 4. Copies core files (cn.ts, themes.ts, ThemeProvider.tsx, index.css)
892
- 5. Wraps {children} with <ThemeProvider> in app/layout.tsx (or pages/_app.tsx)
893
-
894
- ${c.bold}Usage:${c.reset}
895
- ${c.dim}$${c.reset} npx basuicn init
896
- ${c.dim}$${c.reset} npx basuicn init --local ${c.dim}# Use local registry${c.reset}
897
- `,
898
- add: `
899
- ${c.bold}basuicn add${c.reset} ${c.dim}<name...>${c.reset}
900
-
901
- Add one or more components to your project.
902
-
903
- ${c.bold}Options:${c.reset}
904
- ${c.cyan}--force${c.reset} Overwrite existing component files
905
-
906
- ${c.bold}Features:${c.reset}
907
- • Auto-runs init if project hasn't been set up
908
- • Resolves internal dependencies (e.g., dialog depends on button)
909
- • Installs required npm packages automatically
910
- • Patches main entry for components that need it (e.g., toast)
911
-
912
- ${c.bold}Usage:${c.reset}
913
- ${c.dim}$${c.reset} npx basuicn add button
914
- ${c.dim}$${c.reset} npx basuicn add button input card dialog
915
- ${c.dim}$${c.reset} npx basuicn add toast --force
916
-
917
- ${c.bold}Interactive:${c.reset}
918
- ${c.dim}$${c.reset} npx basuicn add ${c.dim}# Prompts to select components${c.reset}
919
- `,
920
- update: `
921
- ${c.bold}basuicn update${c.reset} ${c.dim}<name...>${c.reset}
922
-
923
- Update component(s) to the latest registry version.
924
- Equivalent to ${c.cyan}add --force${c.reset}.
925
-
926
- ${c.bold}Usage:${c.reset}
927
- ${c.dim}$${c.reset} npx basuicn update button
928
- ${c.dim}$${c.reset} npx basuicn update button card dialog
929
- `,
930
- remove: `
931
- ${c.bold}basuicn remove${c.reset} ${c.dim}<name...>${c.reset}
932
-
933
- Remove component(s) from your project.
934
- Deletes component files and cleans up empty directories.
935
-
936
- ${c.bold}Usage:${c.reset}
937
- ${c.dim}$${c.reset} npx basuicn remove button
938
- ${c.dim}$${c.reset} npx basuicn remove dialog drawer sheet
939
- `,
940
- diff: `
941
- ${c.bold}basuicn diff${c.reset} ${c.dim}<name...>${c.reset}
942
-
943
- Show differences between your local component files and the registry version.
944
- Useful to see what has changed before running update.
945
-
946
- ${c.bold}Usage:${c.reset}
947
- ${c.dim}$${c.reset} npx basuicn diff button
948
- ${c.dim}$${c.reset} npx basuicn diff button card
949
- `,
950
- list: `
951
- ${c.bold}basuicn list${c.reset}
952
-
953
- Show all available components in the registry.
954
- Displays internal dependencies for each component.
955
-
956
- ${c.bold}Usage:${c.reset}
957
- ${c.dim}$${c.reset} npx basuicn list
958
- `,
959
- doctor: `
960
- ${c.bold}basuicn doctor${c.reset}
961
-
962
- Run a health check on your project configuration.
963
-
964
- ${c.bold}Checks:${c.reset}
965
- • Core files exist (cn.ts, themes.ts, ThemeProvider.tsx, index.css)
966
- • ThemeProvider + CSS import in main entry
967
- • Runtime packages installed
968
- • Dev packages installed
969
- • Tailwind CSS configured
970
- • TypeScript path aliases
971
- • Vite config present
972
-
973
- ${c.bold}Usage:${c.reset}
974
- ${c.dim}$${c.reset} npx basuicn doctor
975
- `,
976
- };
977
-
978
- // ─── Commands ─────────────────────────────────────────────────────────────────
979
-
980
- const main = async () => {
981
- const args = process.argv.slice(2);
982
-
983
- // Version flag
984
- if (args.includes('--version') || args.includes('-v')) {
985
- console.log(`basuicn v${VERSION}`);
986
- return;
987
- }
988
-
989
- const isLocal = args.includes('--local');
990
- const isForce = args.includes('--force');
991
- const isHelp = args.includes('--help') || args.includes('-h');
992
- const filteredArgs = args.filter((a) => !a.startsWith('--') && a !== '-h' && a !== '-v');
993
- const command = filteredArgs[0];
994
- const componentNames = filteredArgs.slice(1);
995
-
996
- // Help for specific command
997
- if (isHelp && command && HELP_COMMANDS[command]) {
998
- console.log(HELP_COMMANDS[command]);
999
- return;
1000
- }
1001
-
1002
- // General help
1003
- if (isHelp || !command) {
1004
- console.log(HELP_MAIN);
1005
- return;
1006
- }
1007
-
1008
- const cwd = getTargetProjectDir();
1009
- const registry = await getRegistry(isLocal);
1010
-
1011
- switch (command) {
1012
-
1013
- case 'init': {
1014
- log('Initializing project...');
1015
- const framework = detectFramework(cwd);
1016
- if (framework === 'vite') {
1017
- setupViteConfig(cwd);
1018
- } else {
1019
- log(`Detected Next.js (${framework === 'nextjs-app' ? 'App Router' : 'Pages Router'}).`);
1020
- setupNextConfig(cwd);
1021
- }
1022
- setupTsConfig(cwd);
1023
- installNpmPackages(RUNTIME_PACKAGES, cwd);
1024
- ensureCore(registry, cwd, { force: true });
1025
- patchEntryFile(cwd, framework);
1026
- console.log('');
1027
- ok(`${c.bold}Initialization complete!${c.reset} Run ${c.cyan}npx basuicn add <component>${c.reset} to get started.`);
1028
- break;
1029
- }
1030
-
1031
- case 'add': {
1032
- let names = componentNames;
1033
-
1034
- // Interactive mode: no component names provided
1035
- if (names.length === 0) {
1036
- const all = Object.keys(registry.components).sort();
1037
- console.log(`\n${c.bold}Available components (${all.length}):${c.reset}`);
1038
-
1039
- // Group by category
1040
- const categories: Record<string, string[]> = {};
1041
- for (const name of all) {
1042
- const prefix = name.includes('-') ? name.split('-')[0] : 'general';
1043
- if (!categories[prefix]) categories[prefix] = [];
1044
- categories[prefix].push(name);
1045
- }
1046
-
1047
- // Print in columns
1048
- const cols = 4;
1049
- for (let i = 0; i < all.length; i += cols) {
1050
- const row = all.slice(i, i + cols).map(n => n.padEnd(20)).join('');
1051
- console.log(` ${c.dim}${row}${c.reset}`);
1052
- }
1053
-
1054
- console.log('');
1055
- const answer = await ask(`Which components to add? ${c.dim}(space-separated, or "all")${c.reset}`);
1056
- if (!answer) {
1057
- log('No components selected.');
1058
- return;
1059
- }
1060
- names = answer === 'all' ? all : answer.split(/[\s,]+/).filter(Boolean);
1061
- }
1062
-
1063
- // Auto-init if project hasn't been initialized yet
1064
- const cnPath = path.join(cwd, 'src/lib/utils/cn.ts');
1065
- const framework = detectFramework(cwd);
1066
- if (!fs.existsSync(cnPath)) {
1067
- log('Project not initialized — running init first...');
1068
- if (framework === 'vite') {
1069
- setupViteConfig(cwd);
1070
- } else {
1071
- log(`Detected Next.js (${framework === 'nextjs-app' ? 'App Router' : 'Pages Router'}).`);
1072
- setupNextConfig(cwd);
1073
- }
1074
- setupTsConfig(cwd);
1075
- installNpmPackages(RUNTIME_PACKAGES, cwd);
1076
- ensureCore(registry, cwd, { force: true });
1077
- patchEntryFile(cwd, framework);
1078
- console.log('');
1079
- }
1080
-
1081
- for (const name of names) {
1082
- addComponent(name, registry, cwd, { force: isForce, framework });
1083
- patchEntryComponentFile(cwd, name, framework);
1084
- }
1085
- console.log('');
1086
- ok(`${c.bold}Done!${c.reset} Added ${names.length} component(s).`);
1087
- break;
1088
- }
1089
-
1090
- case 'update': {
1091
- if (componentNames.length === 0) {
1092
- error(`Usage: ${c.cyan}npx basuicn update <component-name> [...]${c.reset}`);
1093
- console.log(` Run ${c.cyan}npx basuicn update --help${c.reset} for details.`);
1094
- return;
1095
- }
1096
- const updateFramework = detectFramework(cwd);
1097
- for (const name of componentNames) {
1098
- log(`Updating: ${c.bold}${name}${c.reset}...`);
1099
- addComponent(name, registry, cwd, { force: true, framework: updateFramework });
1100
- }
1101
- console.log('');
1102
- ok(`${c.bold}Update complete.${c.reset}`);
1103
- break;
1104
- }
1105
-
1106
- case 'remove': {
1107
- if (componentNames.length === 0) {
1108
- error(`Usage: ${c.cyan}npx basuicn remove <component-name>${c.reset}`);
1109
- return;
1110
- }
1111
-
1112
- if (!isForce) {
1113
- const yes = await confirm(`Remove ${componentNames.join(', ')}?`);
1114
- if (!yes) {
1115
- log('Cancelled.');
1116
- return;
1117
- }
1118
- }
1119
-
1120
- for (const name of componentNames) {
1121
- removeComponent(name, registry, cwd);
1122
- }
1123
- console.log('');
1124
- ok(`${c.bold}Done!${c.reset}`);
1125
- break;
1126
- }
1127
-
1128
- case 'list': {
1129
- const components = Object.keys(registry.components).sort();
1130
- console.log(`\n${c.bold}Available components (${components.length}):${c.reset}\n`);
1131
-
1132
- const installed: string[] = [];
1133
- const available: string[] = [];
1134
-
1135
- for (const k of components) {
1136
- const comp = registry.components[k];
1137
- const firstFile = comp.files[0];
1138
- const isInstalled = firstFile && fs.existsSync(path.join(cwd, firstFile.path));
1139
- if (isInstalled) installed.push(k);
1140
- else available.push(k);
1141
- }
1142
-
1143
- if (installed.length > 0) {
1144
- console.log(` ${c.green}Installed (${installed.length}):${c.reset}`);
1145
- for (const k of installed) {
1146
- const deps = registry.components[k].internalDependencies?.filter(Boolean);
1147
- const depStr = deps?.length ? ` ${c.dim}→ ${deps.join(', ')}${c.reset}` : '';
1148
- console.log(` ${c.green}●${c.reset} ${k}${depStr}`);
1149
- }
1150
- console.log('');
1151
- }
1152
-
1153
- if (available.length > 0) {
1154
- console.log(` ${c.dim}Available (${available.length}):${c.reset}`);
1155
- for (const k of available) {
1156
- const deps = registry.components[k].internalDependencies?.filter(Boolean);
1157
- const depStr = deps?.length ? ` ${c.dim}→ ${deps.join(', ')}${c.reset}` : '';
1158
- console.log(` ${c.dim}○${c.reset} ${k}${depStr}`);
1159
- }
1160
- }
1161
- console.log('');
1162
- break;
1163
- }
1164
-
1165
- case 'diff': {
1166
- if (componentNames.length === 0) {
1167
- error(`Usage: ${c.cyan}npx basuicn diff <component-name>${c.reset}`);
1168
- return;
1169
- }
1170
- for (const name of componentNames) {
1171
- const component = registry.components[name];
1172
- if (!component) {
1173
- error(`Component "${name}" not found.`);
1174
- continue;
1175
- }
1176
- let hasDiff = false;
1177
- console.log(`\n${c.bold}[diff] ${name}${c.reset}`);
1178
- for (const file of component.files) {
1179
- const targetPath = path.join(cwd, file.path);
1180
- if (!fs.existsSync(targetPath)) {
1181
- console.log(` ${c.green}+ [new file]${c.reset} ${file.path}`);
1182
- hasDiff = true;
1183
- continue;
1184
- }
1185
- const localContent = fs.readFileSync(targetPath, 'utf-8');
1186
- if (localContent === file.content) continue;
1187
- hasDiff = true;
1188
- console.log(`\n ${c.yellow}~${c.reset} ${file.path}`);
1189
- const localLines = localContent.split('\n');
1190
- const remoteLines = file.content.split('\n');
1191
- const maxLen = Math.max(localLines.length, remoteLines.length);
1192
- let shownLines = 0;
1193
- for (let i = 0; i < maxLen; i++) {
1194
- if (localLines[i] !== remoteLines[i]) {
1195
- if (localLines[i] !== undefined) console.log(` ${c.red}- ${localLines[i]}${c.reset}`);
1196
- if (remoteLines[i] !== undefined) console.log(` ${c.green}+ ${remoteLines[i]}${c.reset}`);
1197
- shownLines++;
1198
- if (shownLines >= 20) {
1199
- const remaining = maxLen - i - 1;
1200
- if (remaining > 0) console.log(` ${c.dim}... and ${remaining} more lines${c.reset}`);
1201
- break;
1202
- }
1203
- }
1204
- }
1205
- }
1206
- if (!hasDiff) ok(`${name}: already up to date.`);
1207
- }
1208
- break;
1209
- }
1210
-
1211
- case 'doctor': {
1212
- console.log(`\n${c.bold}Project Health Check${c.reset}\n`);
1213
- let issues = 0;
1214
- const check = (passed: boolean, msg: string, fix?: string) => {
1215
- console.log(` ${passed ? `${c.green}✔${c.reset}` : `${c.red}✖${c.reset}`} ${msg}`);
1216
- if (!passed) { if (fix) console.log(` ${c.dim}→ ${fix}${c.reset}`); issues++; }
1217
- };
1218
-
1219
- const docFramework = detectFramework(cwd);
1220
- if (docFramework !== 'vite') {
1221
- console.log(` ${c.cyan}ℹ${c.reset} Framework: Next.js (${docFramework === 'nextjs-app' ? 'App Router' : 'Pages Router'})\n`);
1222
- }
1223
-
1224
- // Core files
1225
- check(fs.existsSync(path.join(cwd, 'src/lib/utils/cn.ts')),
1226
- 'src/lib/utils/cn.ts', 'run: npx basuicn init');
1227
- check(fs.existsSync(path.join(cwd, 'src/lib/theme/themes.ts')),
1228
- 'src/lib/theme/themes.ts', 'run: npx basuicn init');
1229
- check(fs.existsSync(path.join(cwd, 'src/lib/theme/ThemeProvider.tsx')),
1230
- 'src/lib/theme/ThemeProvider.tsx', 'run: npx basuicn init');
1231
- check(fs.existsSync(path.join(cwd, 'src/styles/index.css')),
1232
- 'src/styles/index.css (theme variables)', 'run: npx basuicn init');
1233
-
1234
- // Entry file check (framework-aware)
1235
- if (docFramework === 'nextjs-app') {
1236
- const layoutPath = findNextLayoutFile(cwd);
1237
- if (layoutPath) {
1238
- const layoutContent = fs.readFileSync(layoutPath, 'utf-8');
1239
- check(layoutContent.includes('ThemeProvider'), 'ThemeProvider in app/layout.tsx', 'run: npx basuicn init');
1240
- check(layoutContent.includes('styles/index.css') || layoutContent.includes('index.css'), 'CSS import in app/layout.tsx', 'run: npx basuicn init');
1241
- } else {
1242
- check(false, 'app/layout.tsx', 'create src/app/layout.tsx');
1243
- }
1244
- } else if (docFramework === 'nextjs-pages') {
1245
- const pagesAppPath = findNextPagesAppFile(cwd);
1246
- if (pagesAppPath) {
1247
- const pagesAppContent = fs.readFileSync(pagesAppPath, 'utf-8');
1248
- check(pagesAppContent.includes('ThemeProvider'), 'ThemeProvider in pages/_app.tsx', 'run: npx basuicn init');
1249
- check(pagesAppContent.includes('styles/index.css') || pagesAppContent.includes('index.css'), 'CSS import in pages/_app.tsx', 'run: npx basuicn init');
1250
- } else {
1251
- check(false, 'pages/_app.tsx', 'create pages/_app.tsx');
1252
- }
1253
- } else {
1254
- const mainPath = findMainFile(cwd);
1255
- if (mainPath) {
1256
- const mainContent = fs.readFileSync(mainPath, 'utf-8');
1257
- check(mainContent.includes('ThemeProvider'), 'ThemeProvider in main entry', 'run: npx basuicn init');
1258
- check(mainContent.includes('styles/index.css') || mainContent.includes('index.css'), 'CSS import in main entry', 'run: npx basuicn init');
1259
- } else {
1260
- check(false, 'main entry file (src/main.tsx)', 'create src/main.tsx');
1261
- }
1262
- }
1263
-
1264
- // Runtime packages
1265
- const pkgPath = path.join(cwd, 'package.json');
1266
- if (fs.existsSync(pkgPath)) {
1267
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as Record<string, unknown>;
1268
- const allDeps = { ...(pkg.dependencies as object || {}), ...(pkg.devDependencies as object || {}) } as Record<string, string>;
1269
- for (const dep of RUNTIME_PACKAGES) {
1270
- check(!!allDeps[dep], `package: ${dep}`, `run: npm install ${dep}`);
1271
- }
1272
- if (docFramework === 'vite') {
1273
- for (const dep of VITE_DEV_PACKAGES) {
1274
- check(!!allDeps[dep], `package (dev): ${dep}`, `run: npm install -D ${dep}`);
1275
- }
1276
- } else {
1277
- for (const dep of NEXTJS_DEV_PACKAGES) {
1278
- check(!!allDeps[dep], `package (dev): ${dep}`, `run: npm install -D ${dep}`);
1279
- }
1280
- }
1281
- } else {
1282
- check(false, 'package.json found', 'run: npm init -y');
1283
- }
1284
-
1285
- // Config files
1286
- const hasTailwindInCss = (() => {
1287
- const candidates = ['src/styles/index.css', 'src/index.css', 'src/App.css'];
1288
- return candidates.some(f => {
1289
- const p = path.join(cwd, f);
1290
- if (!fs.existsSync(p)) return false;
1291
- const content = fs.readFileSync(p, 'utf-8');
1292
- return content.includes('@import "tailwindcss"') || content.includes("@import 'tailwindcss'");
1293
- });
1294
- })();
1295
- check(hasTailwindInCss, '@import "tailwindcss" in CSS', 'run: npx basuicn init');
1296
-
1297
- const tsCandidates = ['tsconfig.app.json', 'tsconfig.json'];
1298
- const hasAlias = tsCandidates.some(f => {
1299
- const p = path.join(cwd, f);
1300
- if (!fs.existsSync(p)) return false;
1301
- const content = fs.readFileSync(p, 'utf-8');
1302
- return content.includes('"@/*"') || content.includes("'@/*'");
1303
- });
1304
- check(hasAlias, 'TypeScript path aliases (@/*)', 'run: npx basuicn init');
1305
-
1306
- if (docFramework === 'vite') {
1307
- const hasViteConfig =
1308
- fs.existsSync(path.join(cwd, 'vite.config.ts')) ||
1309
- fs.existsSync(path.join(cwd, 'vite.config.js'));
1310
- check(hasViteConfig, 'vite.config.ts / vite.config.js', 'run: npx basuicn init');
1311
- } else {
1312
- const hasPostcss = ['postcss.config.mjs', 'postcss.config.js', 'postcss.config.cjs']
1313
- .some(f => fs.existsSync(path.join(cwd, f)));
1314
- check(hasPostcss, 'postcss.config.mjs (Tailwind CSS for Next.js)', 'run: npx basuicn init');
1315
- }
1316
-
1317
- console.log('');
1318
- if (issues === 0) {
1319
- ok(`${c.bold}All checks passed!${c.reset} Project is healthy.`);
1320
- } else {
1321
- warn(`${c.bold}${issues} issue(s) found.${c.reset} Run ${c.cyan}npx basuicn init${c.reset} to fix most issues.`);
1322
- }
1323
- break;
1324
- }
1325
-
1326
- default: {
1327
- error(`Unknown command: "${command}"`);
1328
- console.log(` Run ${c.cyan}npx basuicn --help${c.reset} to see available commands.\n`);
1329
- }
1330
- }
1331
- };
1332
-
1333
- main();
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { execSync } from 'child_process';
5
+ import readline from 'readline';
6
+
7
+ // ─── Constants ────────────────────────────────────────────────────────────────
8
+
9
+ const VERSION = '0.3.19';
10
+ const REGISTRY_LOCAL = './registry.json';
11
+ const REGISTRY_REMOTE = 'https://raw.githubusercontent.com/Basuicn/basuicn-core/main/registry.json';
12
+
13
+ // ─── Colors (ANSI) ───────────────────────────────────────────────────────────
14
+
15
+ const c = {
16
+ reset: '\x1b[0m',
17
+ bold: '\x1b[1m',
18
+ dim: '\x1b[2m',
19
+ green: '\x1b[32m',
20
+ yellow: '\x1b[33m',
21
+ red: '\x1b[31m',
22
+ cyan: '\x1b[36m',
23
+ magenta: '\x1b[35m',
24
+ blue: '\x1b[34m',
25
+ gray: '\x1b[90m',
26
+ };
27
+
28
+ const log = (msg: string) => console.log(`${c.cyan}▸${c.reset} ${msg}`);
29
+ const ok = (msg: string) => console.log(`${c.green}✔${c.reset} ${msg}`);
30
+ const warn = (msg: string) => console.warn(`${c.yellow}⚠${c.reset} ${msg}`);
31
+ const error = (msg: string) => console.error(`${c.red}✖${c.reset} ${msg}`);
32
+
33
+ const getTargetProjectDir = () => process.cwd();
34
+
35
+ // ─── Framework detection ──────────────────────────────────────────────────────
36
+
37
+ type Framework = 'vite' | 'nextjs-app' | 'nextjs-pages';
38
+
39
+ const detectFramework = (cwd: string): Framework => {
40
+ const hasNextConfig =
41
+ fs.existsSync(path.join(cwd, 'next.config.js')) ||
42
+ fs.existsSync(path.join(cwd, 'next.config.ts')) ||
43
+ fs.existsSync(path.join(cwd, 'next.config.mjs'));
44
+
45
+ const hasNextDep = (() => {
46
+ const pkgPath = path.join(cwd, 'package.json');
47
+ if (!fs.existsSync(pkgPath)) return false;
48
+ try {
49
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as Record<string, unknown>;
50
+ const all = { ...(pkg.dependencies as object || {}), ...(pkg.devDependencies as object || {}) } as Record<string, string>;
51
+ return !!all['next'];
52
+ } catch { return false; }
53
+ })();
54
+
55
+ if (hasNextConfig || hasNextDep) {
56
+ const hasAppDir =
57
+ fs.existsSync(path.join(cwd, 'src/app')) ||
58
+ fs.existsSync(path.join(cwd, 'app'));
59
+ const hasPagesDir =
60
+ fs.existsSync(path.join(cwd, 'src/pages')) ||
61
+ fs.existsSync(path.join(cwd, 'pages'));
62
+ if (hasPagesDir && !hasAppDir) return 'nextjs-pages';
63
+ return 'nextjs-app';
64
+ }
65
+
66
+ return 'vite';
67
+ };
68
+
69
+ // ─── Interactive prompt ──────────────────────────────────────────────────────
70
+
71
+ const ask = (question: string): Promise<string> => {
72
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
73
+ return new Promise((resolve) => {
74
+ rl.question(`${c.cyan}?${c.reset} ${question} `, (answer) => {
75
+ rl.close();
76
+ resolve(answer.trim());
77
+ });
78
+ });
79
+ };
80
+
81
+ const confirm = async (question: string, defaultYes = true): Promise<boolean> => {
82
+ const hint = defaultYes ? 'Y/n' : 'y/N';
83
+ const answer = await ask(`${question} ${c.dim}(${hint})${c.reset}`);
84
+ if (!answer) return defaultYes;
85
+ return answer.toLowerCase().startsWith('y');
86
+ };
87
+
88
+ // ─── Registry ─────────────────────────────────────────────────────────────────
89
+
90
+ interface RegistryFile { path: string; content: string }
91
+ interface RegistryComponent {
92
+ dependencies: string[];
93
+ internalDependencies?: string[];
94
+ files: RegistryFile[];
95
+ }
96
+ interface Registry {
97
+ core?: { dependencies: string[]; files: RegistryFile[] };
98
+ components: Record<string, RegistryComponent>;
99
+ }
100
+
101
+ const validateRegistry = (data: unknown): data is Registry => {
102
+ if (!data || typeof data !== 'object') return false;
103
+ const reg = data as Record<string, unknown>;
104
+ return 'components' in reg && typeof reg.components === 'object' && reg.components !== null;
105
+ };
106
+
107
+ const getRegistry = async (isLocal: boolean): Promise<Registry> => {
108
+ if (isLocal && fs.existsSync(REGISTRY_LOCAL)) {
109
+ log('Using local registry...');
110
+ try {
111
+ const data = JSON.parse(fs.readFileSync(REGISTRY_LOCAL, 'utf-8'));
112
+ if (!validateRegistry(data)) {
113
+ error('Invalid local registry format — missing "components" field.');
114
+ process.exit(1);
115
+ }
116
+ return data;
117
+ } catch (err) {
118
+ error(`Failed to parse local registry: ${err instanceof Error ? err.message : err}`);
119
+ process.exit(1);
120
+ }
121
+ }
122
+
123
+ log('Fetching registry from remote...');
124
+ try {
125
+ const response = await fetch(REGISTRY_REMOTE);
126
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
127
+ const data = await response.json();
128
+ if (!validateRegistry(data)) {
129
+ error('Invalid remote registry format — missing "components" field.');
130
+ process.exit(1);
131
+ }
132
+ return data;
133
+ } catch (err: unknown) {
134
+ const message = err instanceof Error ? err.message : String(err);
135
+ error(`Cannot fetch registry: ${message}`);
136
+ process.exit(1);
137
+ }
138
+ };
139
+
140
+ // ─── npm ──────────────────────────────────────────────────────────────────────
141
+
142
+ const installNpmPackages = (packages: string[], cwd: string, dev = false) => {
143
+ if (packages.length === 0) return;
144
+
145
+ const pkgJsonPath = path.join(cwd, 'package.json');
146
+ let toInstall = packages;
147
+
148
+ if (fs.existsSync(pkgJsonPath)) {
149
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
150
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
151
+ toInstall = packages.filter((p) => !allDeps[p]);
152
+ }
153
+
154
+ if (toInstall.length === 0) return;
155
+
156
+ log(`Installing: ${c.bold}${toInstall.join(', ')}${c.reset}...`);
157
+ const flag = dev ? '--save-dev' : '--save';
158
+ try {
159
+ execSync(`npm install ${toInstall.join(' ')} ${flag}`, { stdio: 'inherit', cwd });
160
+ } catch (err) {
161
+ error(`Failed to install packages: ${toInstall.join(', ')}. ${err instanceof Error ? err.message : ''}`);
162
+ }
163
+ };
164
+
165
+ // ─── Packages ─────────────────────────────────────────────────────────────────
166
+
167
+ const VITE_DEV_PACKAGES = [
168
+ 'tailwindcss',
169
+ '@tailwindcss/vite',
170
+ '@vitejs/plugin-react',
171
+ '@types/node',
172
+ ];
173
+
174
+ const RUNTIME_PACKAGES = [
175
+ '@base-ui/react',
176
+ 'tailwind-variants',
177
+ 'clsx',
178
+ 'tailwind-merge',
179
+ 'tailwindcss-animate',
180
+ 'lucide-react',
181
+ ];
182
+
183
+ const NEXTJS_DEV_PACKAGES = [
184
+ 'tailwindcss',
185
+ '@tailwindcss/postcss',
186
+ '@types/node',
187
+ ];
188
+
189
+ const POSTCSS_CONFIG_TEMPLATE = `const config = {
190
+ plugins: {
191
+ '@tailwindcss/postcss': {},
192
+ },
193
+ };
194
+
195
+ export default config;
196
+ `;
197
+
198
+ // ─── Vite config ──────────────────────────────────────────────────────────────
199
+
200
+ const VITE_CONFIG_TEMPLATE = `import { defineConfig } from 'vite';
201
+ import tailwindcss from '@tailwindcss/vite';
202
+ import react from '@vitejs/plugin-react';
203
+ import path from 'path';
204
+
205
+ export default defineConfig({
206
+ plugins: [tailwindcss(), react()],
207
+ resolve: {
208
+ alias: {
209
+ '@': path.resolve(__dirname, './src'),
210
+ '@lib': path.resolve(__dirname, './src/lib'),
211
+ '@components': path.resolve(__dirname, './src/components'),
212
+ '@assets': path.resolve(__dirname, './src/assets'),
213
+ '@pages': path.resolve(__dirname, './src/pages'),
214
+ '@styles': path.resolve(__dirname, './src/styles'),
215
+ },
216
+ },
217
+ });
218
+ `;
219
+
220
+ const TSCONFIG_PATHS = {
221
+ '@/*': ['./src/*'],
222
+ '@lib/*': ['./src/lib/*'],
223
+ '@components/*': ['./src/components/*'],
224
+ '@assets/*': ['./src/assets/*'],
225
+ '@pages/*': ['./src/pages/*'],
226
+ '@styles/*': ['./src/styles/*'],
227
+ };
228
+
229
+ const setupViteConfig = (cwd: string) => {
230
+ installNpmPackages(VITE_DEV_PACKAGES, cwd, true);
231
+
232
+ const configTs = path.join(cwd, 'vite.config.ts');
233
+ const configJs = path.join(cwd, 'vite.config.js');
234
+
235
+ if (!fs.existsSync(configTs) && !fs.existsSync(configJs)) {
236
+ fs.writeFileSync(configTs, VITE_CONFIG_TEMPLATE);
237
+ ok('Created vite.config.ts.');
238
+ return;
239
+ }
240
+
241
+ const existingPath = fs.existsSync(configTs) ? configTs : configJs;
242
+ let content = fs.readFileSync(existingPath, 'utf-8');
243
+
244
+ const missingImports: string[] = [];
245
+ if (!content.includes('@tailwindcss/vite')) missingImports.push("import tailwindcss from '@tailwindcss/vite';");
246
+ if (!content.includes('@vitejs/plugin-react')) missingImports.push("import react from '@vitejs/plugin-react';");
247
+ if (!content.includes("from 'path'") && !content.includes('from "path"')) missingImports.push("import path from 'path';");
248
+
249
+ const missingPlugins: string[] = [];
250
+ if (!content.includes('tailwindcss()')) missingPlugins.push('tailwindcss()');
251
+ if (!content.includes('react()') && !content.includes('react({')) missingPlugins.push('react()');
252
+
253
+ const hasAlias = content.includes('alias:') || content.includes("'@'") || content.includes('"@"');
254
+
255
+ if (missingImports.length === 0 && missingPlugins.length === 0 && hasAlias) {
256
+ ok('vite.config already configured — skipping.');
257
+ return;
258
+ }
259
+
260
+ if (missingImports.length > 0) {
261
+ const importBlock = missingImports.join('\n');
262
+ const allImports = [...content.matchAll(/^import\s.+$/gm)];
263
+ if (allImports.length > 0) {
264
+ const last = allImports[allImports.length - 1];
265
+ const pos = last.index! + last[0].length;
266
+ content = content.slice(0, pos) + '\n' + importBlock + content.slice(pos);
267
+ } else {
268
+ content = importBlock + '\n' + content;
269
+ }
270
+ }
271
+
272
+ if (missingPlugins.length > 0) {
273
+ const match = content.match(/plugins:\s*\[/);
274
+ if (match && match.index !== undefined) {
275
+ const pos = match.index + match[0].length;
276
+ const after = content.slice(pos);
277
+ const pluginLines = missingPlugins.map((p) => `\n ${p},`).join('');
278
+ const needsNewline = after.length > 0 && after[0] !== '\n' && after[0] !== '\r';
279
+ content = content.slice(0, pos) + pluginLines + (needsNewline ? '\n ' : '') + after;
280
+ }
281
+ }
282
+
283
+ if (!hasAlias) {
284
+ const aliasBlock = [
285
+ ' resolve: {',
286
+ ' alias: {',
287
+ " '@': path.resolve(__dirname, './src'),",
288
+ " '@lib': path.resolve(__dirname, './src/lib'),",
289
+ " '@components': path.resolve(__dirname, './src/components'),",
290
+ " '@assets': path.resolve(__dirname, './src/assets'),",
291
+ " '@pages': path.resolve(__dirname, './src/pages'),",
292
+ " '@styles': path.resolve(__dirname, './src/styles'),",
293
+ ' },',
294
+ ' },',
295
+ ].join('\n');
296
+
297
+ const pluginsStart = content.search(/plugins:\s*\[/);
298
+ if (pluginsStart !== -1) {
299
+ let depth = 0;
300
+ let foundStart = false;
301
+ for (let i = pluginsStart; i < content.length; i++) {
302
+ if (content[i] === '[') { depth++; foundStart = true; }
303
+ if (content[i] === ']') depth--;
304
+ if (foundStart && depth === 0) {
305
+ let lineEnd = content.indexOf('\n', i);
306
+ if (lineEnd === -1) lineEnd = content.length;
307
+ content = content.slice(0, lineEnd + 1) + aliasBlock + '\n' + content.slice(lineEnd + 1);
308
+ break;
309
+ }
310
+ }
311
+ }
312
+ }
313
+
314
+ fs.writeFileSync(existingPath, content);
315
+ ok(`Updated ${path.basename(existingPath)} with Tailwind + path aliases.`);
316
+ };
317
+
318
+ // ─── Next.js config ───────────────────────────────────────────────────────────
319
+
320
+ const setupNextConfig = (cwd: string) => {
321
+ installNpmPackages(NEXTJS_DEV_PACKAGES, cwd, true);
322
+
323
+ const postcssCandidates = ['postcss.config.mjs', 'postcss.config.js', 'postcss.config.cjs'];
324
+ const existingPostcss = postcssCandidates.map(f => path.join(cwd, f)).find(p => fs.existsSync(p));
325
+
326
+ if (!existingPostcss) {
327
+ fs.writeFileSync(path.join(cwd, 'postcss.config.mjs'), POSTCSS_CONFIG_TEMPLATE);
328
+ ok('Created postcss.config.mjs with @tailwindcss/postcss.');
329
+ return;
330
+ }
331
+
332
+ const content = fs.readFileSync(existingPostcss, 'utf-8');
333
+ if (!content.includes('@tailwindcss/postcss') && !content.includes('tailwindcss')) {
334
+ warn(`${path.basename(existingPostcss)} found but missing Tailwind plugin — add '@tailwindcss/postcss': {} to plugins manually.`);
335
+ } else {
336
+ ok(`${path.basename(existingPostcss)} already configured — skipping.`);
337
+ }
338
+ };
339
+
340
+ // ─── tsconfig ─────────────────────────────────────────────────────────────────
341
+
342
+ const setupTsConfig = (cwd: string) => {
343
+ const candidates = ['tsconfig.app.json', 'tsconfig.json'];
344
+
345
+ for (const candidate of candidates) {
346
+ const configPath = path.join(cwd, candidate);
347
+ if (!fs.existsSync(configPath)) continue;
348
+
349
+ const raw = fs.readFileSync(configPath, 'utf-8');
350
+
351
+ if (raw.includes('"@/*"') || raw.includes("'@/*'")) {
352
+ ok(`${candidate} already has path aliases — skipping.`);
353
+ return;
354
+ }
355
+
356
+ try {
357
+ const stripped = raw
358
+ .replace(/\/\*[\s\S]*?\*\//g, '')
359
+ .replace(/(^|[\s,{[\]])\/\/[^\n]*/g, '$1');
360
+ const parsed = JSON.parse(stripped) as { compilerOptions?: Record<string, unknown> };
361
+ if (!parsed.compilerOptions) parsed.compilerOptions = {};
362
+ parsed.compilerOptions.baseUrl = '.';
363
+ parsed.compilerOptions.paths = TSCONFIG_PATHS;
364
+ fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2));
365
+ ok(`Added path aliases to ${candidate}.`);
366
+ } catch (err) {
367
+ warn(`Could not auto-patch ${candidate}: ${err instanceof Error ? err.message : err}`);
368
+ warn('Add these to compilerOptions manually:');
369
+ console.log('\n "baseUrl": ".",');
370
+ console.log(' "paths": {');
371
+ for (const [alias, targets] of Object.entries(TSCONFIG_PATHS)) {
372
+ console.log(` "${alias}": ["${targets[0]}"],`);
373
+ }
374
+ console.log(' }');
375
+ console.log('');
376
+ }
377
+ return;
378
+ }
379
+
380
+ const newConfig = { compilerOptions: { baseUrl: '.', paths: TSCONFIG_PATHS } };
381
+ fs.writeFileSync(path.join(cwd, 'tsconfig.json'), JSON.stringify(newConfig, null, 2));
382
+ ok('Created tsconfig.json with path aliases.');
383
+ };
384
+
385
+ // ─── Core files ───────────────────────────────────────────────────────────────
386
+
387
+ const ensureCore = (
388
+ registry: { core?: { dependencies: string[]; files: RegistryFile[] } },
389
+ cwd: string,
390
+ options: { force?: boolean } = {}
391
+ ) => {
392
+ const core = registry.core;
393
+ if (!core) return;
394
+
395
+ installNpmPackages(core.dependencies, cwd);
396
+
397
+ for (const file of core.files) {
398
+ const targetPath = path.join(cwd, file.path);
399
+ const targetDir = path.dirname(targetPath);
400
+
401
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
402
+
403
+ if (fs.existsSync(targetPath) && !options.force) {
404
+ log(`Core file exists (skipping): ${c.dim}${file.path}${c.reset}`);
405
+ continue;
406
+ }
407
+
408
+ fs.writeFileSync(targetPath, file.content);
409
+ ok(`${fs.existsSync(targetPath) ? 'Updated' : 'Created'} core file: ${file.path}`);
410
+ }
411
+ };
412
+
413
+ // ─── Next.js layout patching ─────────────────────────────────────────────────
414
+
415
+ const NEXT_APP_LAYOUT_CANDIDATES = [
416
+ 'src/app/layout.tsx', 'src/app/layout.jsx',
417
+ 'app/layout.tsx', 'app/layout.jsx',
418
+ ];
419
+
420
+ const NEXT_PAGES_APP_CANDIDATES = [
421
+ 'src/pages/_app.tsx', 'src/pages/_app.jsx',
422
+ 'pages/_app.tsx', 'pages/_app.jsx',
423
+ ];
424
+
425
+ const findNextLayoutFile = (cwd: string): string | null => {
426
+ for (const c of NEXT_APP_LAYOUT_CANDIDATES) {
427
+ const p = path.join(cwd, c);
428
+ if (fs.existsSync(p)) return p;
429
+ }
430
+ return null;
431
+ };
432
+
433
+ const findNextPagesAppFile = (cwd: string): string | null => {
434
+ for (const c of NEXT_PAGES_APP_CANDIDATES) {
435
+ const p = path.join(cwd, c);
436
+ if (fs.existsSync(p)) return p;
437
+ }
438
+ return null;
439
+ };
440
+
441
+ const patchNextLayout = (cwd: string) => {
442
+ const layoutPath = findNextLayoutFile(cwd);
443
+ if (!layoutPath) {
444
+ warn('Could not find app/layout.tsx — add ThemeProvider and CSS import manually.');
445
+ return;
446
+ }
447
+
448
+ let content = fs.readFileSync(layoutPath, 'utf-8');
449
+ let changed = false;
450
+
451
+ // CSS import
452
+ const cssImport = "import '@/styles/index.css';";
453
+ if (!content.includes('styles/index.css') && !content.includes('index.css')) {
454
+ const firstImport = content.match(/^import\s/m);
455
+ if (firstImport?.index !== undefined) {
456
+ content = content.slice(0, firstImport.index) + cssImport + '\n' + content.slice(firstImport.index);
457
+ } else {
458
+ content = cssImport + '\n' + content;
459
+ }
460
+ changed = true;
461
+ }
462
+
463
+ // ThemeProvider
464
+ if (!content.includes('ThemeProvider')) {
465
+ content = insertImport(content, "import { ThemeProvider } from '@/lib/theme/ThemeProvider';");
466
+ // Wrap {children} with ThemeProvider
467
+ const wrapped = content.replace(/\{children\}/, '<ThemeProvider>{children}</ThemeProvider>');
468
+ if (wrapped !== content) {
469
+ content = wrapped;
470
+ } else {
471
+ warn('Could not locate {children} in layout.tsx — add <ThemeProvider> wrapper manually.');
472
+ }
473
+ changed = true;
474
+ }
475
+
476
+ if (changed) {
477
+ fs.writeFileSync(layoutPath, content);
478
+ ok(`Patched ${path.relative(cwd, layoutPath)}.`);
479
+ } else {
480
+ ok(`${path.relative(cwd, layoutPath)} already configured — skipping.`);
481
+ }
482
+ };
483
+
484
+ const patchNextPagesApp = (cwd: string) => {
485
+ const appPath = findNextPagesAppFile(cwd);
486
+ if (!appPath) {
487
+ warn('Could not find pages/_app.tsx — add ThemeProvider and CSS import manually.');
488
+ return;
489
+ }
490
+
491
+ let content = fs.readFileSync(appPath, 'utf-8');
492
+ let changed = false;
493
+
494
+ // CSS import
495
+ const cssImport = "import '@/styles/index.css';";
496
+ if (!content.includes('styles/index.css') && !content.includes('index.css')) {
497
+ content = insertImport(content, cssImport);
498
+ changed = true;
499
+ }
500
+
501
+ // ThemeProvider
502
+ if (!content.includes('ThemeProvider')) {
503
+ content = insertImport(content, "import { ThemeProvider } from '@/lib/theme/ThemeProvider';");
504
+ const wrapped = content.replace(/(<Component\s[^/]*\/\s*>)/, '<ThemeProvider>\n $1\n </ThemeProvider>');
505
+ if (wrapped !== content) {
506
+ content = wrapped;
507
+ } else {
508
+ warn('Could not locate <Component .../> in _app.tsx — add <ThemeProvider> wrapper manually.');
509
+ }
510
+ changed = true;
511
+ }
512
+
513
+ if (changed) {
514
+ fs.writeFileSync(appPath, content);
515
+ ok(`Patched ${path.relative(cwd, appPath)}.`);
516
+ } else {
517
+ ok(`${path.relative(cwd, appPath)} already configured — skipping.`);
518
+ }
519
+ };
520
+
521
+ const patchNextLayoutComponent = (cwd: string, patch: { import: string; jsx: string }) => {
522
+ const layoutPath = findNextLayoutFile(cwd);
523
+ if (!layoutPath) {
524
+ warn(`Could not find Next.js layout — add ${patch.jsx} manually.`);
525
+ return;
526
+ }
527
+
528
+ let content = fs.readFileSync(layoutPath, 'utf-8');
529
+ const tagName = patch.jsx.match(/<(\w+)/)?.[1];
530
+ if (tagName && content.includes(`<${tagName}`)) return;
531
+
532
+ content = insertImport(content, patch.import);
533
+ // Insert before </body>
534
+ const updated = content.replace(/<\/body>/, ` ${patch.jsx}\n </body>`);
535
+ if (updated !== content) {
536
+ fs.writeFileSync(layoutPath, updated);
537
+ ok(`Added <${tagName}> to ${path.relative(cwd, layoutPath)}.`);
538
+ } else {
539
+ warn(`Could not auto-add <${tagName}> to layout.tsx — add it manually.`);
540
+ }
541
+ };
542
+
543
+ const patchNextPagesAppComponent = (cwd: string, patch: { import: string; jsx: string }) => {
544
+ const appPath = findNextPagesAppFile(cwd);
545
+ if (!appPath) {
546
+ warn(`Could not find pages/_app.tsx — add ${patch.jsx} manually.`);
547
+ return;
548
+ }
549
+
550
+ let content = fs.readFileSync(appPath, 'utf-8');
551
+ const tagName = patch.jsx.match(/<(\w+)/)?.[1];
552
+ if (tagName && content.includes(`<${tagName}`)) return;
553
+
554
+ content = insertImport(content, patch.import);
555
+ // Insert after <Component .../>
556
+ let updated = content.replace(
557
+ /(<Component\s[^/]*\/\s*>)(\s*\n\s*<\/ThemeProvider>)/,
558
+ `$1\n ${patch.jsx}$2`
559
+ );
560
+ if (updated === content) {
561
+ updated = content.replace(/(<Component\s[^/]*\/\s*>)/, `$1\n ${patch.jsx}`);
562
+ }
563
+ if (updated !== content) {
564
+ fs.writeFileSync(appPath, updated);
565
+ ok(`Added <${tagName}> to ${path.relative(cwd, appPath)}.`);
566
+ } else {
567
+ warn(`Could not auto-add <${tagName}> to _app.tsx — add it manually.`);
568
+ }
569
+ };
570
+
571
+ const patchEntryFile = (cwd: string, framework: Framework) => {
572
+ if (framework === 'nextjs-app') patchNextLayout(cwd);
573
+ else if (framework === 'nextjs-pages') patchNextPagesApp(cwd);
574
+ else patchMainTsx(cwd);
575
+ };
576
+
577
+ const patchEntryComponentFile = (cwd: string, componentName: string, framework: Framework) => {
578
+ const patch = MAIN_PATCH_COMPONENTS[componentName];
579
+ if (!patch) return;
580
+ if (framework === 'nextjs-app') patchNextLayoutComponent(cwd, patch);
581
+ else if (framework === 'nextjs-pages') patchNextPagesAppComponent(cwd, patch);
582
+ else patchMainTsxComponent(cwd, componentName);
583
+ };
584
+
585
+ // ─── main.tsx patching ────────────────────────────────────────────────────────
586
+
587
+ const MAIN_PATCH_COMPONENTS: Record<string, { import: string; jsx: string }> = {
588
+ toast: {
589
+ import: "import { Toaster } from '@/components/ui/toast/Toaster';",
590
+ jsx: '<Toaster position="top-center" expand={true} richColors />',
591
+ },
592
+ };
593
+
594
+ const MAIN_CANDIDATES = ['src/main.tsx', 'src/main.jsx', 'src/index.tsx', 'src/index.jsx'];
595
+
596
+ const findMainFile = (cwd: string): string | null => {
597
+ for (const c of MAIN_CANDIDATES) {
598
+ const p = path.join(cwd, c);
599
+ if (fs.existsSync(p)) return p;
600
+ }
601
+ return null;
602
+ };
603
+
604
+ const insertImport = (content: string, importLine: string): string => {
605
+ if (content.includes(importLine)) return content;
606
+ const allImports = [...content.matchAll(/^import\s.+$/gm)];
607
+ if (allImports.length > 0) {
608
+ const last = allImports[allImports.length - 1];
609
+ const pos = last.index! + last[0].length;
610
+ return content.slice(0, pos) + '\n' + importLine + content.slice(pos);
611
+ }
612
+ return importLine + '\n' + content;
613
+ };
614
+
615
+ const patchMainTsx = (cwd: string) => {
616
+ const mainPath = findMainFile(cwd);
617
+ if (!mainPath) {
618
+ warn('Could not find entry file (src/main.tsx). Skipping main entry setup.');
619
+ return;
620
+ }
621
+
622
+ let content = fs.readFileSync(mainPath, 'utf-8');
623
+ let changed = false;
624
+
625
+ const cssImportLine = "import './styles/index.css';";
626
+ const hasCssImport = content.includes('styles/index.css') || content.includes('index.css');
627
+ if (!hasCssImport) {
628
+ const firstImport = content.match(/^import\s/m);
629
+ if (firstImport?.index !== undefined) {
630
+ content = content.slice(0, firstImport.index) + cssImportLine + '\n' + content.slice(firstImport.index);
631
+ } else {
632
+ content = cssImportLine + '\n' + content;
633
+ }
634
+ changed = true;
635
+ } else if (!content.includes('styles/index.css')) {
636
+ content = insertImport(content, cssImportLine);
637
+ changed = true;
638
+ }
639
+
640
+ if (!content.includes('ThemeProvider')) {
641
+ content = insertImport(content, "import { ThemeProvider } from '@/lib/theme/ThemeProvider';");
642
+
643
+ const wrapped = content.replace(/(<App\s*\/>)/g, '<ThemeProvider>\n $1\n </ThemeProvider>');
644
+ if (wrapped === content) {
645
+ warn('Could not locate <App /> in entry file — add <ThemeProvider> wrapper manually.');
646
+ } else {
647
+ content = wrapped;
648
+ }
649
+ changed = true;
650
+ }
651
+
652
+ if (changed) {
653
+ fs.writeFileSync(mainPath, content);
654
+ ok(`Patched ${path.relative(cwd, mainPath)}.`);
655
+ } else {
656
+ ok(`${path.relative(cwd, mainPath)} already configured — skipping.`);
657
+ }
658
+ };
659
+
660
+ const patchMainTsxComponent = (cwd: string, componentName: string) => {
661
+ const patch = MAIN_PATCH_COMPONENTS[componentName];
662
+ if (!patch) return;
663
+
664
+ const mainPath = findMainFile(cwd);
665
+ if (!mainPath) return;
666
+
667
+ let content = fs.readFileSync(mainPath, 'utf-8');
668
+ const tagName = patch.jsx.match(/<(\w+)/)?.[1];
669
+ if (tagName && content.includes(`<${tagName}`)) return;
670
+
671
+ content = insertImport(content, patch.import);
672
+
673
+ const withProvider = content.replace(
674
+ /(<App\s*\/>)(\s*\n\s*<\/ThemeProvider>)/,
675
+ `$1\n ${patch.jsx}$2`
676
+ );
677
+
678
+ if (withProvider !== content) {
679
+ fs.writeFileSync(mainPath, withProvider);
680
+ } else {
681
+ const fallback = content.replace(/(<App\s*\/>)/, `$1\n ${patch.jsx}`);
682
+ if (fallback !== content) fs.writeFileSync(mainPath, fallback);
683
+ }
684
+
685
+ ok(`Added <${tagName}> to ${path.relative(cwd, mainPath)}.`);
686
+ };
687
+
688
+ // ─── index.ts barrel update ───────────────────────────────────────────────────
689
+
690
+ const UI_INDEX_PATH = 'src/components/ui/index.ts';
691
+ const UI_INDEX_DIR = 'src/components/ui';
692
+
693
+ /** Pick the primary .tsx component file (skip tests, stories, hooks) */
694
+ const pickMainFile = (files: RegistryFile[]): RegistryFile | undefined =>
695
+ files.find(
696
+ (f) =>
697
+ f.path.startsWith(UI_INDEX_DIR + '/') &&
698
+ f.path.endsWith('.tsx') &&
699
+ !f.path.includes('.test.') &&
700
+ !f.path.includes('.stories.'),
701
+ );
702
+
703
+ /** Append `export * from './dir/File';` to index.ts if not already present */
704
+ const addToComponentIndex = (componentFiles: RegistryFile[], cwd: string) => {
705
+ const indexPath = path.join(cwd, UI_INDEX_PATH);
706
+ if (!fs.existsSync(indexPath)) return;
707
+
708
+ const mainFile = pickMainFile(componentFiles);
709
+ if (!mainFile) return;
710
+
711
+ const withoutExt = mainFile.path.replace(/\.tsx$/, '');
712
+ const relPath = './' + path.relative(UI_INDEX_DIR, withoutExt).replace(/\\/g, '/');
713
+ const exportLine = `export * from '${relPath}';`;
714
+
715
+ const content = fs.readFileSync(indexPath, 'utf-8');
716
+ if (content.includes(relPath)) return;
717
+
718
+ fs.writeFileSync(indexPath, content.trimEnd() + '\n' + exportLine + '\n');
719
+ ok(`Updated index.ts: added ${c.dim}${relPath}${c.reset}`);
720
+ };
721
+
722
+ /** Remove the export line from index.ts */
723
+ const removeFromComponentIndex = (componentFiles: RegistryFile[], cwd: string) => {
724
+ const indexPath = path.join(cwd, UI_INDEX_PATH);
725
+ if (!fs.existsSync(indexPath)) return;
726
+
727
+ const mainFile = pickMainFile(componentFiles);
728
+ if (!mainFile) return;
729
+
730
+ const withoutExt = mainFile.path.replace(/\.tsx$/, '');
731
+ const relPath = './' + path.relative(UI_INDEX_DIR, withoutExt).replace(/\\/g, '/');
732
+
733
+ const content = fs.readFileSync(indexPath, 'utf-8');
734
+ const filtered = content
735
+ .split('\n')
736
+ .filter((line) => !line.includes(relPath))
737
+ .join('\n');
738
+
739
+ if (filtered === content) return;
740
+ fs.writeFileSync(indexPath, filtered);
741
+ ok(`Updated index.ts: removed ${c.dim}${relPath}${c.reset}`);
742
+ };
743
+
744
+ // ─── Component add/remove ─────────────────────────────────────────────────────
745
+
746
+ const addComponent = (
747
+ name: string,
748
+ registry: { core?: unknown; components: Record<string, RegistryComponent> },
749
+ cwd: string,
750
+ options: { force: boolean; framework?: Framework },
751
+ added: Set<string> = new Set()
752
+ ) => {
753
+ if (added.has(name)) return;
754
+ added.add(name);
755
+
756
+ const component = registry.components[name];
757
+ if (!component) {
758
+ error(`Component "${name}" not found. Run '${c.cyan}basuicn list${c.reset}' to see available components.`);
759
+ return;
760
+ }
761
+
762
+ log(`Adding: ${c.bold}${name}${c.reset}...`);
763
+
764
+ ensureCore(registry as Parameters<typeof ensureCore>[0], cwd);
765
+ installNpmPackages(component.dependencies, cwd);
766
+
767
+ if (component.internalDependencies) {
768
+ for (const dep of component.internalDependencies) {
769
+ if (registry.components[dep]) {
770
+ addComponent(dep, registry, cwd, options, added);
771
+ }
772
+ }
773
+ }
774
+
775
+ for (const file of component.files) {
776
+ const targetPath = path.join(cwd, file.path);
777
+ const targetDir = path.dirname(targetPath);
778
+
779
+ if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
780
+
781
+ if (fs.existsSync(targetPath) && !options.force) {
782
+ warn(`Skipped (exists): ${file.path} — use ${c.cyan}--force${c.reset} to overwrite`);
783
+ continue;
784
+ }
785
+
786
+ // Add 'use client' for Next.js App Router TSX components that don't already have it
787
+ let content = file.content;
788
+ if (options.framework === 'nextjs-app' && file.path.endsWith('.tsx')) {
789
+ if (!content.startsWith("'use client'") && !content.startsWith('"use client"')) {
790
+ content = "'use client';\n" + content;
791
+ }
792
+ }
793
+
794
+ fs.writeFileSync(targetPath, content);
795
+ ok(`Created: ${file.path}`);
796
+ }
797
+
798
+ addToComponentIndex(component.files, cwd);
799
+ };
800
+
801
+ const removeComponent = (
802
+ name: string,
803
+ registry: { components: Record<string, { files: { path: string }[] }> },
804
+ cwd: string
805
+ ) => {
806
+ const component = registry.components[name];
807
+ if (!component) {
808
+ error(`Component "${name}" not found.`);
809
+ return;
810
+ }
811
+
812
+ log(`Removing: ${c.bold}${name}${c.reset}...`);
813
+
814
+ for (const file of component.files) {
815
+ const targetPath = path.join(cwd, file.path);
816
+ if (fs.existsSync(targetPath)) {
817
+ fs.unlinkSync(targetPath);
818
+ ok(`Deleted: ${file.path}`);
819
+ }
820
+ }
821
+
822
+ for (const file of component.files) {
823
+ const targetDir = path.dirname(path.join(cwd, file.path));
824
+ try {
825
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length === 0) {
826
+ fs.rmdirSync(targetDir);
827
+ ok(`Removed empty dir: ${path.relative(cwd, targetDir)}`);
828
+ }
829
+ } catch (err) {
830
+ warn(`Could not remove directory: ${err instanceof Error ? err.message : err}`);
831
+ }
832
+ }
833
+
834
+ removeFromComponentIndex(component.files as RegistryFile[], cwd);
835
+ };
836
+
837
+ // ─── Help texts ───────────────────────────────────────────────────────────────
838
+
839
+ const HELP_MAIN = `
840
+ ${c.bold}${c.cyan}basuicn${c.reset} ${c.dim}v${VERSION}${c.reset} — Modern React UI Component CLI
841
+
842
+ ${c.bold}USAGE${c.reset}
843
+ ${c.cyan}npx basuicn${c.reset} ${c.green}<command>${c.reset} ${c.dim}[options]${c.reset}
844
+
845
+ ${c.bold}COMMANDS${c.reset}
846
+ ${c.green}init${c.reset} Initialize project: install deps, copy core files, patch entry
847
+ ${c.green}add${c.reset} ${c.dim}<name...>${c.reset} Add component(s) to your project
848
+ ${c.green}update${c.reset} ${c.dim}<name...>${c.reset} Update component(s) to latest registry version
849
+ ${c.green}diff${c.reset} ${c.dim}<name...>${c.reset} Show diff between local and registry version
850
+ ${c.green}remove${c.reset} ${c.dim}<name...>${c.reset} Remove component(s) from your project
851
+ ${c.green}list${c.reset} List all available components
852
+ ${c.green}doctor${c.reset} Check project health and configuration
853
+
854
+ ${c.bold}OPTIONS${c.reset}
855
+ ${c.cyan}--force${c.reset} Overwrite existing files when adding/updating
856
+ ${c.cyan}--local${c.reset} Use local registry.json instead of remote
857
+ ${c.cyan}--help, -h${c.reset} Show help (use with a command for detailed help)
858
+ ${c.cyan}--version, -v${c.reset} Show version
859
+
860
+ ${c.bold}QUICK START${c.reset}
861
+ ${c.dim}$${c.reset} npx basuicn init
862
+ ${c.dim}$${c.reset} npx basuicn add button input card
863
+ ${c.dim}$${c.reset} npx basuicn add toast
864
+
865
+ ${c.bold}EXAMPLES${c.reset}
866
+ ${c.dim}$${c.reset} npx basuicn add dialog --force ${c.dim}# Overwrite existing dialog${c.reset}
867
+ ${c.dim}$${c.reset} npx basuicn diff button ${c.dim}# See what changed since last update${c.reset}
868
+ ${c.dim}$${c.reset} npx basuicn doctor ${c.dim}# Diagnose missing deps/config${c.reset}
869
+
870
+ ${c.dim}Documentation: https://github.com/Basuicn/basuicn-core${c.reset}
871
+ `;
872
+
873
+ const HELP_COMMANDS: Record<string, string> = {
874
+ init: `
875
+ ${c.bold}basuicn init${c.reset}
876
+
877
+ Initialize your project for basuicn components.
878
+ Auto-detects Vite or Next.js (App Router / Pages Router).
879
+
880
+ ${c.bold}What it does (Vite):${c.reset}
881
+ 1. Installs runtime dependencies (@base-ui/react, tailwind-variants, etc.)
882
+ 2. Sets up vite.config.ts with Tailwind CSS + path aliases
883
+ 3. Patches tsconfig.json with path aliases (@/*, @lib/*, etc.)
884
+ 4. Copies core files (cn.ts, themes.ts, ThemeProvider.tsx, index.css)
885
+ 5. Wraps your <App /> with <ThemeProvider> in src/main.tsx
886
+
887
+ ${c.bold}What it does (Next.js):${c.reset}
888
+ 1. Installs runtime dependencies (@base-ui/react, tailwind-variants, etc.)
889
+ 2. Sets up postcss.config.mjs with @tailwindcss/postcss
890
+ 3. Patches tsconfig.json with path aliases (@/*, @lib/*, etc.)
891
+ 4. Copies core files (cn.ts, themes.ts, ThemeProvider.tsx, index.css)
892
+ 5. Wraps {children} with <ThemeProvider> in app/layout.tsx (or pages/_app.tsx)
893
+
894
+ ${c.bold}Usage:${c.reset}
895
+ ${c.dim}$${c.reset} npx basuicn init
896
+ ${c.dim}$${c.reset} npx basuicn init --local ${c.dim}# Use local registry${c.reset}
897
+ `,
898
+ add: `
899
+ ${c.bold}basuicn add${c.reset} ${c.dim}<name...>${c.reset}
900
+
901
+ Add one or more components to your project.
902
+
903
+ ${c.bold}Options:${c.reset}
904
+ ${c.cyan}--force${c.reset} Overwrite existing component files
905
+
906
+ ${c.bold}Features:${c.reset}
907
+ • Auto-runs init if project hasn't been set up
908
+ • Resolves internal dependencies (e.g., dialog depends on button)
909
+ • Installs required npm packages automatically
910
+ • Patches main entry for components that need it (e.g., toast)
911
+
912
+ ${c.bold}Usage:${c.reset}
913
+ ${c.dim}$${c.reset} npx basuicn add button
914
+ ${c.dim}$${c.reset} npx basuicn add button input card dialog
915
+ ${c.dim}$${c.reset} npx basuicn add toast --force
916
+
917
+ ${c.bold}Interactive:${c.reset}
918
+ ${c.dim}$${c.reset} npx basuicn add ${c.dim}# Prompts to select components${c.reset}
919
+ `,
920
+ update: `
921
+ ${c.bold}basuicn update${c.reset} ${c.dim}<name...>${c.reset}
922
+
923
+ Update component(s) to the latest registry version.
924
+ Equivalent to ${c.cyan}add --force${c.reset}.
925
+
926
+ ${c.bold}Usage:${c.reset}
927
+ ${c.dim}$${c.reset} npx basuicn update button
928
+ ${c.dim}$${c.reset} npx basuicn update button card dialog
929
+ `,
930
+ remove: `
931
+ ${c.bold}basuicn remove${c.reset} ${c.dim}<name...>${c.reset}
932
+
933
+ Remove component(s) from your project.
934
+ Deletes component files and cleans up empty directories.
935
+
936
+ ${c.bold}Usage:${c.reset}
937
+ ${c.dim}$${c.reset} npx basuicn remove button
938
+ ${c.dim}$${c.reset} npx basuicn remove dialog drawer sheet
939
+ `,
940
+ diff: `
941
+ ${c.bold}basuicn diff${c.reset} ${c.dim}<name...>${c.reset}
942
+
943
+ Show differences between your local component files and the registry version.
944
+ Useful to see what has changed before running update.
945
+
946
+ ${c.bold}Usage:${c.reset}
947
+ ${c.dim}$${c.reset} npx basuicn diff button
948
+ ${c.dim}$${c.reset} npx basuicn diff button card
949
+ `,
950
+ list: `
951
+ ${c.bold}basuicn list${c.reset}
952
+
953
+ Show all available components in the registry.
954
+ Displays internal dependencies for each component.
955
+
956
+ ${c.bold}Usage:${c.reset}
957
+ ${c.dim}$${c.reset} npx basuicn list
958
+ `,
959
+ doctor: `
960
+ ${c.bold}basuicn doctor${c.reset}
961
+
962
+ Run a health check on your project configuration.
963
+
964
+ ${c.bold}Checks:${c.reset}
965
+ • Core files exist (cn.ts, themes.ts, ThemeProvider.tsx, index.css)
966
+ • ThemeProvider + CSS import in main entry
967
+ • Runtime packages installed
968
+ • Dev packages installed
969
+ • Tailwind CSS configured
970
+ • TypeScript path aliases
971
+ • Vite config present
972
+
973
+ ${c.bold}Usage:${c.reset}
974
+ ${c.dim}$${c.reset} npx basuicn doctor
975
+ `,
976
+ };
977
+
978
+ // ─── Commands ─────────────────────────────────────────────────────────────────
979
+
980
+ const main = async () => {
981
+ const args = process.argv.slice(2);
982
+
983
+ // Version flag
984
+ if (args.includes('--version') || args.includes('-v')) {
985
+ console.log(`basuicn v${VERSION}`);
986
+ return;
987
+ }
988
+
989
+ const isLocal = args.includes('--local');
990
+ const isForce = args.includes('--force');
991
+ const isHelp = args.includes('--help') || args.includes('-h');
992
+ const filteredArgs = args.filter((a) => !a.startsWith('--') && a !== '-h' && a !== '-v');
993
+ const command = filteredArgs[0];
994
+ const componentNames = filteredArgs.slice(1);
995
+
996
+ // Help for specific command
997
+ if (isHelp && command && HELP_COMMANDS[command]) {
998
+ console.log(HELP_COMMANDS[command]);
999
+ return;
1000
+ }
1001
+
1002
+ // General help
1003
+ if (isHelp || !command) {
1004
+ console.log(HELP_MAIN);
1005
+ return;
1006
+ }
1007
+
1008
+ const cwd = getTargetProjectDir();
1009
+ const registry = await getRegistry(isLocal);
1010
+
1011
+ switch (command) {
1012
+
1013
+ case 'init': {
1014
+ log('Initializing project...');
1015
+ const framework = detectFramework(cwd);
1016
+ if (framework === 'vite') {
1017
+ setupViteConfig(cwd);
1018
+ } else {
1019
+ log(`Detected Next.js (${framework === 'nextjs-app' ? 'App Router' : 'Pages Router'}).`);
1020
+ setupNextConfig(cwd);
1021
+ }
1022
+ setupTsConfig(cwd);
1023
+ installNpmPackages(RUNTIME_PACKAGES, cwd);
1024
+ ensureCore(registry, cwd, { force: true });
1025
+ patchEntryFile(cwd, framework);
1026
+ console.log('');
1027
+ ok(`${c.bold}Initialization complete!${c.reset} Run ${c.cyan}npx basuicn add <component>${c.reset} to get started.`);
1028
+ break;
1029
+ }
1030
+
1031
+ case 'add': {
1032
+ let names = componentNames;
1033
+
1034
+ // Interactive mode: no component names provided
1035
+ if (names.length === 0) {
1036
+ const all = Object.keys(registry.components).sort();
1037
+ console.log(`\n${c.bold}Available components (${all.length}):${c.reset}`);
1038
+
1039
+ // Group by category
1040
+ const categories: Record<string, string[]> = {};
1041
+ for (const name of all) {
1042
+ const prefix = name.includes('-') ? name.split('-')[0] : 'general';
1043
+ if (!categories[prefix]) categories[prefix] = [];
1044
+ categories[prefix].push(name);
1045
+ }
1046
+
1047
+ // Print in columns
1048
+ const cols = 4;
1049
+ for (let i = 0; i < all.length; i += cols) {
1050
+ const row = all.slice(i, i + cols).map(n => n.padEnd(20)).join('');
1051
+ console.log(` ${c.dim}${row}${c.reset}`);
1052
+ }
1053
+
1054
+ console.log('');
1055
+ const answer = await ask(`Which components to add? ${c.dim}(space-separated, or "all")${c.reset}`);
1056
+ if (!answer) {
1057
+ log('No components selected.');
1058
+ return;
1059
+ }
1060
+ names = answer === 'all' ? all : answer.split(/[\s,]+/).filter(Boolean);
1061
+ }
1062
+
1063
+ // Auto-init if project hasn't been initialized yet
1064
+ const cnPath = path.join(cwd, 'src/lib/utils/cn.ts');
1065
+ const framework = detectFramework(cwd);
1066
+ if (!fs.existsSync(cnPath)) {
1067
+ log('Project not initialized — running init first...');
1068
+ if (framework === 'vite') {
1069
+ setupViteConfig(cwd);
1070
+ } else {
1071
+ log(`Detected Next.js (${framework === 'nextjs-app' ? 'App Router' : 'Pages Router'}).`);
1072
+ setupNextConfig(cwd);
1073
+ }
1074
+ setupTsConfig(cwd);
1075
+ installNpmPackages(RUNTIME_PACKAGES, cwd);
1076
+ ensureCore(registry, cwd, { force: true });
1077
+ patchEntryFile(cwd, framework);
1078
+ console.log('');
1079
+ }
1080
+
1081
+ for (const name of names) {
1082
+ addComponent(name, registry, cwd, { force: isForce, framework });
1083
+ patchEntryComponentFile(cwd, name, framework);
1084
+ }
1085
+ console.log('');
1086
+ ok(`${c.bold}Done!${c.reset} Added ${names.length} component(s).`);
1087
+ break;
1088
+ }
1089
+
1090
+ case 'update': {
1091
+ if (componentNames.length === 0) {
1092
+ error(`Usage: ${c.cyan}npx basuicn update <component-name> [...]${c.reset}`);
1093
+ console.log(` Run ${c.cyan}npx basuicn update --help${c.reset} for details.`);
1094
+ return;
1095
+ }
1096
+ const updateFramework = detectFramework(cwd);
1097
+ for (const name of componentNames) {
1098
+ log(`Updating: ${c.bold}${name}${c.reset}...`);
1099
+ addComponent(name, registry, cwd, { force: true, framework: updateFramework });
1100
+ }
1101
+ console.log('');
1102
+ ok(`${c.bold}Update complete.${c.reset}`);
1103
+ break;
1104
+ }
1105
+
1106
+ case 'remove': {
1107
+ if (componentNames.length === 0) {
1108
+ error(`Usage: ${c.cyan}npx basuicn remove <component-name>${c.reset}`);
1109
+ return;
1110
+ }
1111
+
1112
+ if (!isForce) {
1113
+ const yes = await confirm(`Remove ${componentNames.join(', ')}?`);
1114
+ if (!yes) {
1115
+ log('Cancelled.');
1116
+ return;
1117
+ }
1118
+ }
1119
+
1120
+ for (const name of componentNames) {
1121
+ removeComponent(name, registry, cwd);
1122
+ }
1123
+ console.log('');
1124
+ ok(`${c.bold}Done!${c.reset}`);
1125
+ break;
1126
+ }
1127
+
1128
+ case 'list': {
1129
+ const components = Object.keys(registry.components).sort();
1130
+ console.log(`\n${c.bold}Available components (${components.length}):${c.reset}\n`);
1131
+
1132
+ const installed: string[] = [];
1133
+ const available: string[] = [];
1134
+
1135
+ for (const k of components) {
1136
+ const comp = registry.components[k];
1137
+ const firstFile = comp.files[0];
1138
+ const isInstalled = firstFile && fs.existsSync(path.join(cwd, firstFile.path));
1139
+ if (isInstalled) installed.push(k);
1140
+ else available.push(k);
1141
+ }
1142
+
1143
+ if (installed.length > 0) {
1144
+ console.log(` ${c.green}Installed (${installed.length}):${c.reset}`);
1145
+ for (const k of installed) {
1146
+ const deps = registry.components[k].internalDependencies?.filter(Boolean);
1147
+ const depStr = deps?.length ? ` ${c.dim}→ ${deps.join(', ')}${c.reset}` : '';
1148
+ console.log(` ${c.green}●${c.reset} ${k}${depStr}`);
1149
+ }
1150
+ console.log('');
1151
+ }
1152
+
1153
+ if (available.length > 0) {
1154
+ console.log(` ${c.dim}Available (${available.length}):${c.reset}`);
1155
+ for (const k of available) {
1156
+ const deps = registry.components[k].internalDependencies?.filter(Boolean);
1157
+ const depStr = deps?.length ? ` ${c.dim}→ ${deps.join(', ')}${c.reset}` : '';
1158
+ console.log(` ${c.dim}○${c.reset} ${k}${depStr}`);
1159
+ }
1160
+ }
1161
+ console.log('');
1162
+ break;
1163
+ }
1164
+
1165
+ case 'diff': {
1166
+ if (componentNames.length === 0) {
1167
+ error(`Usage: ${c.cyan}npx basuicn diff <component-name>${c.reset}`);
1168
+ return;
1169
+ }
1170
+ for (const name of componentNames) {
1171
+ const component = registry.components[name];
1172
+ if (!component) {
1173
+ error(`Component "${name}" not found.`);
1174
+ continue;
1175
+ }
1176
+ let hasDiff = false;
1177
+ console.log(`\n${c.bold}[diff] ${name}${c.reset}`);
1178
+ for (const file of component.files) {
1179
+ const targetPath = path.join(cwd, file.path);
1180
+ if (!fs.existsSync(targetPath)) {
1181
+ console.log(` ${c.green}+ [new file]${c.reset} ${file.path}`);
1182
+ hasDiff = true;
1183
+ continue;
1184
+ }
1185
+ const localContent = fs.readFileSync(targetPath, 'utf-8');
1186
+ if (localContent === file.content) continue;
1187
+ hasDiff = true;
1188
+ console.log(`\n ${c.yellow}~${c.reset} ${file.path}`);
1189
+ const localLines = localContent.split('\n');
1190
+ const remoteLines = file.content.split('\n');
1191
+ const maxLen = Math.max(localLines.length, remoteLines.length);
1192
+ let shownLines = 0;
1193
+ for (let i = 0; i < maxLen; i++) {
1194
+ if (localLines[i] !== remoteLines[i]) {
1195
+ if (localLines[i] !== undefined) console.log(` ${c.red}- ${localLines[i]}${c.reset}`);
1196
+ if (remoteLines[i] !== undefined) console.log(` ${c.green}+ ${remoteLines[i]}${c.reset}`);
1197
+ shownLines++;
1198
+ if (shownLines >= 20) {
1199
+ const remaining = maxLen - i - 1;
1200
+ if (remaining > 0) console.log(` ${c.dim}... and ${remaining} more lines${c.reset}`);
1201
+ break;
1202
+ }
1203
+ }
1204
+ }
1205
+ }
1206
+ if (!hasDiff) ok(`${name}: already up to date.`);
1207
+ }
1208
+ break;
1209
+ }
1210
+
1211
+ case 'doctor': {
1212
+ console.log(`\n${c.bold}Project Health Check${c.reset}\n`);
1213
+ let issues = 0;
1214
+ const check = (passed: boolean, msg: string, fix?: string) => {
1215
+ console.log(` ${passed ? `${c.green}✔${c.reset}` : `${c.red}✖${c.reset}`} ${msg}`);
1216
+ if (!passed) { if (fix) console.log(` ${c.dim}→ ${fix}${c.reset}`); issues++; }
1217
+ };
1218
+
1219
+ const docFramework = detectFramework(cwd);
1220
+ if (docFramework !== 'vite') {
1221
+ console.log(` ${c.cyan}ℹ${c.reset} Framework: Next.js (${docFramework === 'nextjs-app' ? 'App Router' : 'Pages Router'})\n`);
1222
+ }
1223
+
1224
+ // Core files
1225
+ check(fs.existsSync(path.join(cwd, 'src/lib/utils/cn.ts')),
1226
+ 'src/lib/utils/cn.ts', 'run: npx basuicn init');
1227
+ check(fs.existsSync(path.join(cwd, 'src/lib/theme/themes.ts')),
1228
+ 'src/lib/theme/themes.ts', 'run: npx basuicn init');
1229
+ check(fs.existsSync(path.join(cwd, 'src/lib/theme/ThemeProvider.tsx')),
1230
+ 'src/lib/theme/ThemeProvider.tsx', 'run: npx basuicn init');
1231
+ check(fs.existsSync(path.join(cwd, 'src/styles/index.css')),
1232
+ 'src/styles/index.css (theme variables)', 'run: npx basuicn init');
1233
+
1234
+ // Entry file check (framework-aware)
1235
+ if (docFramework === 'nextjs-app') {
1236
+ const layoutPath = findNextLayoutFile(cwd);
1237
+ if (layoutPath) {
1238
+ const layoutContent = fs.readFileSync(layoutPath, 'utf-8');
1239
+ check(layoutContent.includes('ThemeProvider'), 'ThemeProvider in app/layout.tsx', 'run: npx basuicn init');
1240
+ check(layoutContent.includes('styles/index.css') || layoutContent.includes('index.css'), 'CSS import in app/layout.tsx', 'run: npx basuicn init');
1241
+ } else {
1242
+ check(false, 'app/layout.tsx', 'create src/app/layout.tsx');
1243
+ }
1244
+ } else if (docFramework === 'nextjs-pages') {
1245
+ const pagesAppPath = findNextPagesAppFile(cwd);
1246
+ if (pagesAppPath) {
1247
+ const pagesAppContent = fs.readFileSync(pagesAppPath, 'utf-8');
1248
+ check(pagesAppContent.includes('ThemeProvider'), 'ThemeProvider in pages/_app.tsx', 'run: npx basuicn init');
1249
+ check(pagesAppContent.includes('styles/index.css') || pagesAppContent.includes('index.css'), 'CSS import in pages/_app.tsx', 'run: npx basuicn init');
1250
+ } else {
1251
+ check(false, 'pages/_app.tsx', 'create pages/_app.tsx');
1252
+ }
1253
+ } else {
1254
+ const mainPath = findMainFile(cwd);
1255
+ if (mainPath) {
1256
+ const mainContent = fs.readFileSync(mainPath, 'utf-8');
1257
+ check(mainContent.includes('ThemeProvider'), 'ThemeProvider in main entry', 'run: npx basuicn init');
1258
+ check(mainContent.includes('styles/index.css') || mainContent.includes('index.css'), 'CSS import in main entry', 'run: npx basuicn init');
1259
+ } else {
1260
+ check(false, 'main entry file (src/main.tsx)', 'create src/main.tsx');
1261
+ }
1262
+ }
1263
+
1264
+ // Runtime packages
1265
+ const pkgPath = path.join(cwd, 'package.json');
1266
+ if (fs.existsSync(pkgPath)) {
1267
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as Record<string, unknown>;
1268
+ const allDeps = { ...(pkg.dependencies as object || {}), ...(pkg.devDependencies as object || {}) } as Record<string, string>;
1269
+ for (const dep of RUNTIME_PACKAGES) {
1270
+ check(!!allDeps[dep], `package: ${dep}`, `run: npm install ${dep}`);
1271
+ }
1272
+ if (docFramework === 'vite') {
1273
+ for (const dep of VITE_DEV_PACKAGES) {
1274
+ check(!!allDeps[dep], `package (dev): ${dep}`, `run: npm install -D ${dep}`);
1275
+ }
1276
+ } else {
1277
+ for (const dep of NEXTJS_DEV_PACKAGES) {
1278
+ check(!!allDeps[dep], `package (dev): ${dep}`, `run: npm install -D ${dep}`);
1279
+ }
1280
+ }
1281
+ } else {
1282
+ check(false, 'package.json found', 'run: npm init -y');
1283
+ }
1284
+
1285
+ // Config files
1286
+ const hasTailwindInCss = (() => {
1287
+ const candidates = ['src/styles/index.css', 'src/index.css', 'src/App.css'];
1288
+ return candidates.some(f => {
1289
+ const p = path.join(cwd, f);
1290
+ if (!fs.existsSync(p)) return false;
1291
+ const content = fs.readFileSync(p, 'utf-8');
1292
+ return content.includes('@import "tailwindcss"') || content.includes("@import 'tailwindcss'");
1293
+ });
1294
+ })();
1295
+ check(hasTailwindInCss, '@import "tailwindcss" in CSS', 'run: npx basuicn init');
1296
+
1297
+ const tsCandidates = ['tsconfig.app.json', 'tsconfig.json'];
1298
+ const hasAlias = tsCandidates.some(f => {
1299
+ const p = path.join(cwd, f);
1300
+ if (!fs.existsSync(p)) return false;
1301
+ const content = fs.readFileSync(p, 'utf-8');
1302
+ return content.includes('"@/*"') || content.includes("'@/*'");
1303
+ });
1304
+ check(hasAlias, 'TypeScript path aliases (@/*)', 'run: npx basuicn init');
1305
+
1306
+ if (docFramework === 'vite') {
1307
+ const hasViteConfig =
1308
+ fs.existsSync(path.join(cwd, 'vite.config.ts')) ||
1309
+ fs.existsSync(path.join(cwd, 'vite.config.js'));
1310
+ check(hasViteConfig, 'vite.config.ts / vite.config.js', 'run: npx basuicn init');
1311
+ } else {
1312
+ const hasPostcss = ['postcss.config.mjs', 'postcss.config.js', 'postcss.config.cjs']
1313
+ .some(f => fs.existsSync(path.join(cwd, f)));
1314
+ check(hasPostcss, 'postcss.config.mjs (Tailwind CSS for Next.js)', 'run: npx basuicn init');
1315
+ }
1316
+
1317
+ console.log('');
1318
+ if (issues === 0) {
1319
+ ok(`${c.bold}All checks passed!${c.reset} Project is healthy.`);
1320
+ } else {
1321
+ warn(`${c.bold}${issues} issue(s) found.${c.reset} Run ${c.cyan}npx basuicn init${c.reset} to fix most issues.`);
1322
+ }
1323
+ break;
1324
+ }
1325
+
1326
+ default: {
1327
+ error(`Unknown command: "${command}"`);
1328
+ console.log(` Run ${c.cyan}npx basuicn --help${c.reset} to see available commands.\n`);
1329
+ }
1330
+ }
1331
+ };
1332
+
1333
+ main();