diff --git a/bun.lock b/bun.lock index 636f78e..ff88c63 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "betterbase", @@ -67,9 +66,11 @@ }, "dependencies": { "chalk": "^5.3.0", + "cli-table3": "^0.6.5", "commander": "^12.1.0", "inquirer": "^10.2.2", "nanoid": "^5.0.0", + "ora": "^8.0.0", "postgres": "^3.4.0", "zod": "^3.23.8", }, @@ -348,6 +349,8 @@ "@bufbuild/protobuf": ["@bufbuild/protobuf@2.11.0", "", {}, "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ=="], + "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], @@ -1156,7 +1159,7 @@ "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], - "ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -1202,6 +1205,12 @@ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], @@ -1336,6 +1345,8 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], @@ -1380,6 +1391,10 @@ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], @@ -1430,6 +1445,8 @@ "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -1440,6 +1457,8 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -1478,6 +1497,10 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], @@ -1574,6 +1597,8 @@ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + "rollup": ["rollup@4.60.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.0", "@rollup/rollup-android-arm64": "4.60.0", "@rollup/rollup-darwin-arm64": "4.60.0", "@rollup/rollup-darwin-x64": "4.60.0", "@rollup/rollup-freebsd-arm64": "4.60.0", "@rollup/rollup-freebsd-x64": "4.60.0", "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", "@rollup/rollup-linux-arm-musleabihf": "4.60.0", "@rollup/rollup-linux-arm64-gnu": "4.60.0", "@rollup/rollup-linux-arm64-musl": "4.60.0", "@rollup/rollup-linux-loong64-gnu": "4.60.0", "@rollup/rollup-linux-loong64-musl": "4.60.0", "@rollup/rollup-linux-ppc64-gnu": "4.60.0", "@rollup/rollup-linux-ppc64-musl": "4.60.0", "@rollup/rollup-linux-riscv64-gnu": "4.60.0", "@rollup/rollup-linux-riscv64-musl": "4.60.0", "@rollup/rollup-linux-s390x-gnu": "4.60.0", "@rollup/rollup-linux-x64-gnu": "4.60.0", "@rollup/rollup-linux-x64-musl": "4.60.0", "@rollup/rollup-openbsd-x64": "4.60.0", "@rollup/rollup-openharmony-arm64": "4.60.0", "@rollup/rollup-win32-arm64-msvc": "4.60.0", "@rollup/rollup-win32-ia32-msvc": "4.60.0", "@rollup/rollup-win32-x64-gnu": "4.60.0", "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], @@ -1616,11 +1641,13 @@ "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], @@ -1836,12 +1863,18 @@ "inngest/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "inngest/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], + "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], + "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "next/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "ora/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "pg/pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -1946,6 +1979,8 @@ "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "inngest/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], + "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "next/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -1986,6 +2021,8 @@ "next/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "protobufjs/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/packages/cli/package.json b/packages/cli/package.json index 0a7016d..f88892e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -18,7 +18,9 @@ "inquirer": "^10.2.2", "nanoid": "^5.0.0", "postgres": "^3.4.0", - "zod": "^3.23.8" + "zod": "^3.23.8", + "cli-table3": "^0.6.5", + "ora": "^8.0.0" }, "devDependencies": { "@types/bun": "^1.3.9", diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index b41345d..bf8ba0e 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -2,7 +2,7 @@ import { existsSync } from "fs"; import { join, relative } from "path"; import chalk from "chalk"; import { ContextGenerator } from "../utils/context-generator"; -import { error, info, success, warn } from "../utils/logger"; +import { blank, error, info, keyValue, sym, warn } from "../utils/logger"; import { ProcessManager } from "./dev/process-manager"; import { queryLog } from "./dev/query-log"; import { DevWatcher } from "./dev/watcher"; @@ -13,8 +13,15 @@ export async function runDevCommand(projectRoot: string) { const hasBetterBase = existsSync(join(projectRoot, "betterbase")); const hasIaC = hasBetterBase; - // Print banner - console.log(chalk.bold.cyan("\n BetterBase Dev\n")); + blank(); + console.log(chalk.bold(" bb dev") + chalk.dim(" โ€” watching for changes")); + blank(); + keyValue("Project root", projectRoot); + keyValue("Server URL", "http://localhost:3000"); + keyValue("Dashboard", "http://localhost:3000/admin"); + blank(); + console.log(chalk.dim(" Press Ctrl+C to stop")); + blank(); if (hasIaC) { info("IaC layer detected โ€” betterbase/ will be watched for schema and function changes."); } @@ -54,7 +61,9 @@ export async function runDevCommand(projectRoot: string) { switch (event.kind) { case "schema": { - info(`[iac] Schema changed: ${label}`); + console.log( + ` ${chalk.dim(new Date().toLocaleTimeString("en-US", { hour12: false }))} ${chalk.yellow("~")} ${chalk.dim(label)} ${chalk.dim("โ†’ regenerating context")}`, + ); const result = await runIacSync(projectRoot, { force: false, silent: false }).catch( (e: Error) => { warn(`[iac] ${e.message}`); @@ -94,9 +103,17 @@ export async function runDevCommand(projectRoot: string) { } // Regenerate context on every change - ctxGen.generate(projectRoot).catch((e: Error) => { + const startedAt = Date.now(); + ctxGen.generate(projectRoot) + .then(() => { + const elapsed = Date.now() - startedAt; + console.log( + ` ${chalk.dim(new Date().toLocaleTimeString("en-US", { hour12: false }))} ${chalk.green(sym.success)} context updated ${chalk.dim(`(${elapsed}ms)`)}`, + ); + }) + .catch((e: Error) => { warn(`Context regeneration failed: ${e.message}`); - }); + }); }); watcher.start(projectRoot); diff --git a/packages/cli/src/commands/function.ts b/packages/cli/src/commands/function.ts index f7cdec4..e865e11 100644 --- a/packages/cli/src/commands/function.ts +++ b/packages/cli/src/commands/function.ts @@ -17,7 +17,9 @@ import { getVercelLogs, syncEnvToCloudflare, } from "@betterbase/core/functions"; +import chalk from "chalk"; import * as logger from "../utils/logger"; +import { createSpinner, withSpinner } from "../utils/spinner"; // Store running function processes for cleanup const runningFunctions: Map = new Map(); @@ -380,9 +382,18 @@ async function runFunctionDeploy( return; } - // First, build the function - console.log(`Building function "${name}" before deployment...`); - const buildResult = await bundleFunction(name, projectRoot); + const config = await readFunctionConfig(name, projectRoot); + const runtime = config?.runtime ?? "cloudflare-workers"; + logger.section(`Deploying ${chalk.cyan(name)}`); + logger.keyValue("Target", runtime); + logger.keyValue("Function", name); + logger.blank(); + + const buildResult = await withSpinner( + "Bundling function...", + async () => await bundleFunction(name, projectRoot), + { successText: `Bundled ${chalk.dim(`dist/${name}.js`)}` }, + ); if (!buildResult.success) { logger.error("Build failed:"); @@ -391,16 +402,11 @@ async function runFunctionDeploy( } return; } - - console.log(`Build successful (${(buildResult.size / 1024).toFixed(2)} KB)\n`); - - // Get function config - const config = await readFunctionConfig(name, projectRoot); - const runtime = config?.runtime ?? "cloudflare-workers"; - - console.log(`Deploying to ${runtime}...`); + logger.info(`Bundle size: ${(buildResult.size / 1024).toFixed(2)} KB`); let deployResult: DeployResult | undefined; + const spinner = createSpinner("Deploying to edge...").start(); + spinner.text = `Deploying to ${runtime}...`; if (runtime === "cloudflare-workers") { deployResult = await deployToCloudflare( @@ -419,15 +425,24 @@ async function runFunctionDeploy( } if (!deployResult.success) { + spinner.stop(); logger.error("Deployment failed:"); for (const log of deployResult.logs) { console.log(` ${log}`); } return; } + spinner.stopAndPersist({ + symbol: chalk.green(logger.sym.success), + text: `Deployed ${chalk.cyan(name)}`, + }); - console.log("\nDeployment successful!"); - console.log(` URL: ${deployResult.url}`); + logger.blank(); + logger.box("Deployment complete", [ + { label: "Function", value: name }, + { label: "Target", value: runtime }, + { label: "URL", value: deployResult.url ?? "pending" }, + ]); // Handle env sync if (syncEnv && config && config.env.length > 0) { diff --git a/packages/cli/src/commands/generate.ts b/packages/cli/src/commands/generate.ts index da8f77a..f7e51a8 100644 --- a/packages/cli/src/commands/generate.ts +++ b/packages/cli/src/commands/generate.ts @@ -1,7 +1,9 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import path from "node:path"; +import chalk from "chalk"; import * as logger from "../utils/logger"; import { SchemaScanner, type TableInfo } from "../utils/schema-scanner"; +import { withSpinner } from "../utils/spinner"; import { runGenerateGraphqlCommand } from "./graphql"; function toSingular(name: string): string { @@ -345,9 +347,16 @@ export async function runGenerateCrudCommand( } logger.info(`Generating CRUD for ${tableName}...`); + logger.section(`Generating CRUD for "${tableName}"`); + logger.tree([`src/routes/${tableName}.ts`, "Updated src/routes/index.ts"]); + logger.blank(); const scanner = new SchemaScanner(schemaPath); - const tables = scanner.scan(); + const tables = await withSpinner( + "Scanning schema...", + async () => scanner.scan(), + { successText: `Found table ${chalk.cyan(tableName)}` }, + ); const table = tables[tableName]; if (!table) { throw new Error(`Table "${tableName}" not found in schema.`); @@ -360,16 +369,34 @@ export async function runGenerateCrudCommand( mkdirSync(routesDir, { recursive: true }); const routePath = path.join(routesDir, `${tableName}.ts`); - writeFileSync(routePath, generateRouteFile(tableName, table)); - - updateMainRouter(resolvedRoot, tableName); - - logger.success(`Generated ${routePath}`); - logger.info(`GET /api/${tableName}`); - logger.info(`GET /api/${tableName}/:id`); - logger.info(`POST /api/${tableName}`); - logger.info(`PATCH /api/${tableName}/:id`); - logger.info(`DELETE /api/${tableName}/:id`); + await withSpinner( + "Writing route file...", + async () => { + writeFileSync(routePath, generateRouteFile(tableName, table)); + }, + { successText: `Created ${chalk.cyan(`src/routes/${tableName}.ts`)}` }, + ); + + await withSpinner( + "Updating router index...", + async () => updateMainRouter(resolvedRoot, tableName), + { successText: "Router updated" }, + ); + + logger.blank(); + logger.section("Generated endpoints"); + [ + ["GET", `/api/${tableName}`, "List all (paginated)"], + ["GET", `/api/${tableName}/:id`, "Get single"], + ["POST", `/api/${tableName}`, "Create"], + ["PATCH", `/api/${tableName}/:id`, "Update"], + ["DELETE", `/api/${tableName}/:id`, "Delete"], + ].forEach(([method, endpoint, desc]) => { + const color = { GET: chalk.green, POST: chalk.blue, PATCH: chalk.yellow, DELETE: chalk.red }[ + method + ]!; + console.log(` ${color(method.padEnd(7))} ${chalk.white(endpoint.padEnd(28))} ${chalk.dim(desc)}`); + }); // Regenerate GraphQL schema after CRUD generation logger.info("Regenerating GraphQL schema..."); diff --git a/packages/cli/src/commands/iac/sync.ts b/packages/cli/src/commands/iac/sync.ts index aec52f6..a3f87b9 100644 --- a/packages/cli/src/commands/iac/sync.ts +++ b/packages/cli/src/commands/iac/sync.ts @@ -5,12 +5,14 @@ import { generateMigration } from "@betterbase/core/iac"; import { generateDrizzleSchema } from "@betterbase/core/iac"; import chalk from "chalk"; import { mkdir, readdir, writeFile } from "fs/promises"; -import { error, info, success, warn } from "../../utils/logger"; +import { done, error, info, section, success, sym, warn } from "../../utils/logger"; +import { withSpinner } from "../../utils/spinner"; export async function runIacSync( projectRoot: string, opts: { force?: boolean; silent?: boolean } = {}, ) { + const startTime = Date.now(); const betterbaseDir = join(projectRoot, "betterbase"); const schemaFile = join(betterbaseDir, "schema.ts"); const prevFile = join(betterbaseDir, "_generated", "schema.json"); @@ -43,8 +45,26 @@ export async function runIacSync( } if (!opts.silent) { + section("IaC Sync"); info("Pending schema changes:"); console.log(formatDiff(diff)); + const grouped = { + added: diff.changes.filter((c) => c.type.includes("add") || c.type.includes("create")), + modified: diff.changes.filter((c) => c.type.includes("alter") || c.type.includes("modify")), + removed: diff.changes.filter((c) => c.type.includes("drop") || c.type.includes("remove")), + }; + if (grouped.added.length) { + console.log(` ${chalk.green("+ Added tables:")}`); + grouped.added.forEach((c) => console.log(` ${chalk.green(sym.bullet)} ${c.table}`)); + } + if (grouped.modified.length) { + console.log(` ${chalk.yellow("~ Modified tables:")}`); + grouped.modified.forEach((c) => console.log(` ${chalk.yellow(sym.bullet)} ${c.table}`)); + } + if (grouped.removed.length) { + console.log(` ${chalk.red("- Removed tables:")}`); + grouped.removed.forEach((c) => console.log(` ${chalk.red(sym.bullet)} ${c.table}`)); + } } if (diff.hasDestructive && !opts.force) { @@ -70,9 +90,19 @@ export async function runIacSync( await writeFile(join(migrDir, migration.filename), migration.sql); if (!opts.silent) info(`Migration written: ${migration.filename}`); - const drizzleCode = generateDrizzleSchema(current, "postgres"); - await writeFile(drizzleOut, drizzleCode); - if (!opts.silent) info("Drizzle schema updated: src/db/schema.generated.ts"); + if (opts.silent) { + const drizzleCode = generateDrizzleSchema(current, "postgres"); + await writeFile(drizzleOut, drizzleCode); + } else { + await withSpinner( + "Generating Drizzle schema...", + async () => { + const drizzleCode = generateDrizzleSchema(current, "postgres"); + await writeFile(drizzleOut, drizzleCode); + }, + { successText: "Schema generated" }, + ); + } await mkdir(genDir, { recursive: true }); await saveSerializedSchema(current, prevFile); @@ -80,5 +110,6 @@ export async function runIacSync( if (!opts.silent) { info("Run the migration runner to apply changes to the database."); success("IaC sync complete."); + done(startTime, "Schema synced"); } } diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index fa01acc..5a0d59b 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -1,6 +1,7 @@ import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; import path from "node:path"; import { generateDrizzleConfig } from "@betterbase/core/config"; +import chalk from "chalk"; import { z } from "zod"; import * as logger from "../utils/logger"; import * as prompts from "../utils/prompts"; @@ -1299,6 +1300,9 @@ export default server; */ export async function runInitCommand(rawOptions: InitCommandOptions): Promise { const options = initOptionsSchema.parse(rawOptions); + logger.blank(); + console.log(chalk.bold(" Create a new Betterbase project")); + logger.blank(); // Default: IaC mode (no flag passed means iac = true) // --no-iac flag means iac = false (legacy interactive mode) @@ -1316,20 +1320,23 @@ export async function runInitCommand(rawOptions: InitCommandOptions): Promise { + console.log(` ${chalk.dim(`${idx + 1}.`)} ${item}`); + }); + logger.blank(); } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error(`Failed to create IaC project: ${message}`); @@ -1470,17 +1477,28 @@ export async function runInitCommand(rawOptions: InitCommandOptions): Promise { + console.log(` ${chalk.dim(`${idx + 1}.`)} ${item}`); + }); + logger.blank(); } catch (error) { if (createdProjectDir) { try { diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts index 787dd6a..22e270e 100644 --- a/packages/cli/src/commands/login.ts +++ b/packages/cli/src/commands/login.ts @@ -1,7 +1,8 @@ import chalk from "chalk"; import type { Command } from "commander"; import { clearCredentials, loadCredentials, saveCredentials } from "../utils/credentials"; -import { error, info, success } from "../utils/logger"; +import { blank, box, error, keyValue, section, success, sym } from "../utils/logger"; +import { createSpinner } from "../utils/spinner"; const DEFAULT_SERVER_URL = "https://api.betterbase.io"; const POLL_INTERVAL_MS = 5000; @@ -28,7 +29,8 @@ export function registerLoginCommand(program: Command) { export async function runLoginCommand(opts: { serverUrl?: string } = {}) { const serverUrl = (opts.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/$/, ""); - info(`Logging in to ${chalk.cyan(serverUrl)} ...`); + blank(); + section("Authorize CLI"); // Step 1: Request device code let deviceCode: string; @@ -51,18 +53,22 @@ export async function runLoginCommand(opts: { serverUrl?: string } = {}) { process.exit(1); } - console.log(""); - console.log(chalk.bold("Open this URL in your browser to authorize:")); - console.log(chalk.cyan(`${verificationUri}?code=${userCode}`)); - console.log(""); - console.log(`Your code: ${chalk.yellow.bold(userCode)}`); - console.log("Waiting for authorization..."); + keyValue("Instance", serverUrl); + keyValue("Your code", chalk.bold(chalk.yellow(userCode))); + blank(); + console.log(` ${chalk.dim("Open:")} ${chalk.cyan(`${verificationUri}?code=${userCode}`)}`); + blank(); + console.log(chalk.dim(" Waiting for browser authorization") + chalk.dim(" (5 min timeout)...")); // Step 2: Poll for token const deadline = Date.now() + POLL_TIMEOUT_MS; + const startedAt = Date.now(); + const spinner = createSpinner("Waiting for authorization...").start(); while (Date.now() < deadline) { await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS)); + const elapsed = Date.now() - startedAt; + spinner.text = `Waiting for authorization ${chalk.dim(`(${Math.round(elapsed / 1000)}s)`)}`; const res = await fetch(`${serverUrl}/device/token`, { method: "POST", @@ -75,6 +81,7 @@ export async function runLoginCommand(opts: { serverUrl?: string } = {}) { if (!res.ok) { const body = (await res.json()) as { error?: string }; if (body.error === "authorization_pending") continue; + spinner.stop(); error(`Login failed: ${body.error ?? "unknown error"}`); process.exit(1); } @@ -94,10 +101,17 @@ export async function runLoginCommand(opts: { serverUrl?: string } = {}) { created_at: new Date().toISOString(), }); + spinner.stopAndPersist({ symbol: chalk.green(sym.success), text: "Authorized" }); + blank(); + box("Logged in", [ + { label: "Instance", value: serverUrl }, + { label: "Account", value: admin.email }, + ]); success(`Logged in as ${chalk.cyan(admin.email)}`); return; } + spinner.stop(); error("Login timed out. Please try again."); process.exit(1); } diff --git a/packages/cli/src/commands/migrate.ts b/packages/cli/src/commands/migrate.ts index ca1291d..83d6d8e 100644 --- a/packages/cli/src/commands/migrate.ts +++ b/packages/cli/src/commands/migrate.ts @@ -6,6 +6,7 @@ import { z } from "zod"; import { DEFAULT_DB_PATH } from "../constants"; import * as logger from "../utils/logger"; import * as prompts from "../utils/prompts"; +import { withSpinner } from "../utils/spinner"; import { runGenerateGraphqlCommand } from "./graphql"; import { calculateChecksum, @@ -257,10 +258,14 @@ async function confirmDestructive(changes: MigrationChange[]): Promise const destructive = changes.filter((c) => c.isDestructive); if (destructive.length === 0) return true; - logger.warn("DESTRUCTIVE CHANGES DETECTED:"); + logger.blank(); + console.log(chalk.yellow(logger.sym.warn) + " " + chalk.yellow.bold("Destructive operations detected:")); for (const change of destructive) { - console.log(` - ${change.type}: ${change.table}${change.column ? `.${change.column}` : ""}`); + console.log( + ` ${chalk.red(logger.sym.bullet)} ${change.type}: ${change.table}${change.column ? `.${change.column}` : ""}`, + ); } + logger.blank(); const confirmation = await prompts.text({ message: 'Type "delete data" to confirm:', @@ -435,10 +440,14 @@ async function collectChangesFromGenerate(): Promise { } export async function runMigrateCommand(rawOptions: MigrateCommandOptions): Promise { + const startTime = Date.now(); const options = migrateOptionsSchema.parse(rawOptions); - logger.info("Generating migration files with drizzle-kit..."); - const changes = await collectChangesFromGenerate(); + const changes = await withSpinner( + "Generating migration files...", + async () => await collectChangesFromGenerate(), + { successText: "Migration files generated" }, + ); displayDiff(changes); if (options.preview) { @@ -464,9 +473,12 @@ export async function runMigrateCommand(rawOptions: MigrateCommandOptions): Prom if (!confirmed) return; } - logger.info("Applying migrations with drizzle-kit push..."); logger.info("drizzle/ files are for preview; running push will apply changes."); - const push = await runDrizzleKit(["push"]); + const push = await withSpinner( + "Applying migration changes...", + async () => await runDrizzleKit(["push"]), + { successText: "Applied migration changes" }, + ); if (!push.success) { await restoreBackup(backup); @@ -484,8 +496,7 @@ export async function runMigrateCommand(rawOptions: MigrateCommandOptions): Prom throw new Error(`Migration push failed.\n${push.stderr || push.stdout}`); } - logger.info("drizzle-kit push completed; changes applied."); - logger.success("Migration complete!"); + logger.done(startTime, "Migration complete"); // Regenerate GraphQL schema after migration // Use the directory where the migration was run (current working directory) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 29b7493..ce7127e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,4 +1,5 @@ import { Command, CommanderError } from "commander"; +import chalk from "chalk"; import packageJson from "../package.json"; import { runAuthAddProviderCommand, runAuthSetupCommand } from "./commands/auth"; import { runBranchCommand } from "./commands/branch"; @@ -58,14 +59,62 @@ async function checkAuthHook(): Promise { */ export function createProgram(): Command { const program = new Command(); + const isDebug = process.argv.includes("--debug"); program .name("bb") .description("BetterBase CLI") .version(packageJson.version, "-v, --version", "display the CLI version") + .option("--debug", "Show full error stack traces") .exitOverride() .hook("preAction", checkAuthHook); + program.configureOutput({ + writeErr: (str) => { + logger.error(str.replace(/^error: /i, "").trim()); + }, + }); + program.configureHelp({ + sortSubcommands: true, + helpWidth: 80, + subcommandTerm: (cmd) => chalk.cyan(cmd.name()), + optionTerm: (opt) => chalk.yellow(opt.flags), + }); + program.addHelpText( + "before", + `\n${chalk.bold(" bb")} ${chalk.dim("โ€” Betterbase CLI")}\n\n ${chalk.dim("Manage projects, schema, functions, and deployments.")}\n`, + ); + program.addHelpText( + "after", + `\n ${chalk.dim("Examples:")}\n ${chalk.dim("$")} bb init my-app\n ${chalk.dim("$")} bb dev\n ${chalk.dim("$")} bb iac sync\n ${chalk.dim("$")} bb login --url http://localhost:3001\n\n ${chalk.dim("Docs:")} ${chalk.cyan("https://docs.betterbase.io/cli")}\n`, + ); + + const getErrorHint = (err: unknown): string | undefined => { + const msg = err instanceof Error ? err.message : String(err); + if (msg.includes("ENOENT")) + return "File not found โ€” check that you're in a Betterbase project directory"; + if (msg.includes("ECONNREFUSED")) return "Could not reach server โ€” is it running?"; + if (msg.includes("Unauthorized")) return "Run `bb login` to authenticate"; + if (msg.includes("MODULE_NOT_FOUND")) return "Run `bun install` to install dependencies"; + if (msg.includes("DATABASE_URL")) return "Set DATABASE_URL in your .env file"; + return undefined; + }; + process.on("uncaughtException", (err) => { + logger.blank(); + logger.error(err.message, getErrorHint(err)); + if (isDebug) console.error(chalk.dim(err.stack)); + logger.blank(); + process.exit(1); + }); + process.on("unhandledRejection", (reason: unknown) => { + const err = reason instanceof Error ? reason : new Error(String(reason)); + logger.blank(); + logger.error(err.message, getErrorHint(err)); + if (isDebug) console.error(chalk.dim(err.stack)); + logger.blank(); + process.exit(1); + }); + program .command("init") .description("Initialize a BetterBase project with BetterBase template (betterbase/ functions)") diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts index 81dae6b..49b19ee 100644 --- a/packages/cli/src/utils/logger.ts +++ b/packages/cli/src/utils/logger.ts @@ -1,51 +1,107 @@ import chalk from "chalk"; -const isTest = process.env.NODE_ENV === "test" || process.argv[1]?.includes("bun"); +const IS_UNICODE = + process.platform !== "win32" || Boolean(process.env.CI) || Boolean(process.env.WT_SESSION); -function formatInfo(message: string): string { - if (isTest) return message; - return `โ„น ${message}`; +export const sym = { + success: IS_UNICODE ? "โœ“" : "+", + error: IS_UNICODE ? "โœ—" : "x", + warn: IS_UNICODE ? "โš " : "!", + info: IS_UNICODE ? "โ—†" : "*", + arrow: IS_UNICODE ? "โ†’" : "->", + bullet: IS_UNICODE ? "โ€ข" : "-", + tree: IS_UNICODE ? "โ”œโ”€" : "|-", + treeLast: IS_UNICODE ? "โ””โ”€" : "\\-", + dot: IS_UNICODE ? "ยท" : ".", +}; + +export function success(msg: string): void { + console.log(`${chalk.green(sym.success)} ${msg}`); +} + +export function error(msg: string, hint?: string): void { + console.error(`${chalk.red(sym.error)} ${chalk.red(msg)}`); + if (hint) { + console.error(` ${chalk.dim(hint)}`); + } +} + +export function warn(msg: string): void { + console.warn(`${chalk.yellow(sym.warn)} ${chalk.yellow(msg)}`); +} + +export function info(msg: string): void { + console.log(`${chalk.cyan(sym.info)} ${msg}`); +} + +export function dim(msg: string): void { + console.log(chalk.dim(msg)); +} + +export function step(n: number, total: number, msg: string): void { + const badgeValue = chalk.bgCyan.black(` ${n}/${total} `); + console.log(`${badgeValue} ${msg}`); +} + +export function section(title: string): void { + console.log(""); + console.log(chalk.bold(chalk.white(title))); + console.log(chalk.dim("โ”€".repeat(Math.min(title.length + 2, 60)))); } -function formatWarn(message: string): string { - if (isTest) return message; - return `โš  ${message}`; +export function keyValue(key: string, value: string, opts?: { secret?: boolean }): void { + const displayed = opts?.secret ? chalk.dim("โ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข") : chalk.cyan(value); + console.log(` ${chalk.dim(key.padEnd(22))} ${displayed}`); } -function formatError(message: string): string { - if (isTest) return message; - return `โœ– ${message}`; +export function tree(items: string[]): void { + items.forEach((item, i) => { + const isLast = i === items.length - 1; + const prefix = isLast ? sym.treeLast : sym.tree; + console.log(` ${chalk.dim(prefix)} ${item}`); + }); } -function formatSuccess(message: string): string { - if (isTest) return message; - return `โœ” ${message}`; +export function blank(): void { + console.log(""); } -/** - * Print an informational message to stderr. - */ -export function info(message: string): void { - console.error(chalk.blue(formatInfo(message))); +export function banner(version: string): void { + console.log(""); + console.log(chalk.bold(chalk.white(" betterbase")) + chalk.dim(` v${version}`)); + console.log(chalk.dim(" AI-native Backend-as-a-Service")); + console.log(""); } -/** - * Print a warning message to stderr. - */ -export function warn(message: string): void { - console.error(chalk.yellow(formatWarn(message))); +export function box(title: string, lines: { label: string; value: string }[]): void { + const width = 60; + const border = chalk.dim("โ”€".repeat(width)); + console.log(""); + console.log(chalk.dim("โ”Œ") + border + chalk.dim("โ”")); + console.log(chalk.dim("โ”‚") + chalk.bold(` ${title}`).padEnd(width + 9) + chalk.dim("โ”‚")); + console.log(chalk.dim("โ”œ") + border + chalk.dim("โ”ค")); + for (const line of lines) { + const label = chalk.dim(line.label.padEnd(18)); + const value = chalk.cyan(line.value); + const content = ` ${label} ${value}`; + console.log(chalk.dim("โ”‚") + content.padEnd(width + 12) + chalk.dim("โ”‚")); + } + console.log(chalk.dim("โ””") + border + chalk.dim("โ”˜")); + console.log(""); } -/** - * Print an error message to stderr. - */ -export function error(message: string): void { - console.error(chalk.red(formatError(message))); +export function badge(text: string, color: "green" | "red" | "yellow" | "blue" | "dim"): string { + const map = { + green: chalk.bgGreen.black, + red: chalk.bgRed.white, + yellow: chalk.bgYellow.black, + blue: chalk.bgBlue.white, + dim: chalk.bgGray.white, + }; + return map[color](` ${text} `); } -/** - * Print a success message to stderr. - */ -export function success(message: string): void { - console.error(chalk.green(formatSuccess(message))); +export function done(startMs: number, msg?: string): void { + const elapsed = ((Date.now() - startMs) / 1000).toFixed(2); + console.log(`\n${chalk.green(sym.success)} ${msg ?? "Done"} ${chalk.dim(`(${elapsed}s)`)}`); } diff --git a/packages/cli/src/utils/spinner.ts b/packages/cli/src/utils/spinner.ts new file mode 100644 index 0000000..f41cc80 --- /dev/null +++ b/packages/cli/src/utils/spinner.ts @@ -0,0 +1,37 @@ +import chalk from "chalk"; +import ora, { type Ora } from "ora"; +import { sym } from "./logger"; + +export function createSpinner(text: string): Ora { + return ora({ + text, + color: "cyan", + spinner: { + interval: 80, + frames: ["โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ‡", "โ "], + }, + }); +} + +export async function withSpinner( + text: string, + task: (spinner: Ora) => Promise, + opts?: { successText?: string; failText?: string }, +): Promise { + const spinner = createSpinner(text).start(); + try { + const result = await task(spinner); + spinner.stopAndPersist({ + symbol: chalk.green(sym.success), + text: opts?.successText ?? text, + }); + return result; + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + spinner.stopAndPersist({ + symbol: chalk.red(sym.error), + text: `${opts?.failText ?? text}: ${chalk.red(message)}`, + }); + throw err; + } +}