From 7faa6699912822a442c116e4375786fdb8b60b6c Mon Sep 17 00:00:00 2001 From: Nazar Kornienko Date: Sun, 2 Feb 2025 00:25:00 +0100 Subject: [PATCH] improve template repo download logic --- .reliverse | 30 +- build.config.ts | 11 +- bun.lock | 152 +++- cspell.json | 2 +- package.json | 12 +- pub.config.ts | 10 +- pub.setup.ts | 364 +++++++--- schema.json | 15 +- src/app/app-impl.ts | 6 +- src/app/app-mod.ts | 10 +- src/app/constants.ts | 2 +- src/app/menu/create-project/cp-impl.ts | 46 +- src/app/menu/create-project/cp-mod.ts | 2 + .../cli-menu-items/getMainMenuOptions.ts | 38 +- .../cli-main-modules/configs/env.test.ts | 9 +- .../modules/showStartEndPrompt.ts | 14 +- .../cp-modules/compose-env-file/cef-mod.ts | 4 +- .../git-deploy-prompts/vercel/vercel-team.ts | 4 +- .../use-composer-mode/template/base/pkg.json | 2 +- src/app/menu/menu-mod.ts | 60 +- src/args/login/login-impl.ts | 6 +- src/args/login/login-mod.ts | 4 +- src/args/memory/memory-mod.ts | 4 +- src/utils/bun-windows/bw-impl.ts | 16 +- src/utils/bun-windows/bw-mod.ts | 13 +- src/utils/downloading/downloadRepo.ts | 312 +++++--- src/utils/downloading/handleDownload.ts | 20 +- src/utils/gdp-mod.ts | 4 +- src/utils/handlers/generateProjectConfigs.ts | 1 + src/utils/handlers/handleCodemods.ts | 2 +- src/utils/reliverseConfig.ts | 667 +++++++++--------- src/utils/reliverseMemory.ts | 2 +- src/utils/schemaConfig.ts | 14 +- 33 files changed, 1131 insertions(+), 727 deletions(-) diff --git a/.reliverse b/.reliverse index 50d93b3..a214ff0 100644 --- a/.reliverse +++ b/.reliverse @@ -8,23 +8,24 @@ "projectName": "@reliverse/cli", "projectAuthor": "blefnk", "projectDescription": "This superapp CLI tool can help you easily create new web projects, manage existing projects, and automatically make advanced codebase modifications, with more features coming soon.", - "projectVersion": "1.4.47", + "projectVersion": "1.4.60", "projectLicense": "MIT", - "projectRepository": "unknown/@reliverse/cli", "projectState": "creating", + "projectRepository": "https://github.com/reliverse/cli", "projectDomain": "https://docs.reliverse.org", - "projectDeployService": "vercel", "projectCategory": "unknown", "projectSubcategory": "unknown", "projectTemplate": "unknown", "projectArchitecture": "unknown", "repoPrivacy": "unknown", + "projectGitService": "github", + "projectDeployService": "vercel", "repoBranch": "main", // Primary tech stack/framework "projectFramework": "npm-jsr", "projectPackageManager": "bun", - "projectRuntime": "nodejs", + "projectRuntime": "bun", "preferredLibraries": { "stateManagement": "zustand", "formManagement": "react-hook-form", @@ -41,7 +42,7 @@ "sharedPackages": [] }, - // Dependencies to exclude from checks + // List dependencies to exclude from checks "ignoreDependencies": [], // Custom rules for Reliverse AI @@ -96,32 +97,29 @@ } }, - // `Clone an existing repo` menu + // Settings for cloning an existing repo "multipleRepoCloneMode": false, "customUserFocusedRepos": [], "customDevsFocusedRepos": [], "hideRepoSuggestions": false, "customReposOnNewProject": false, - // Set to false to disable opening - // the browser while env composing + // Set to false to disable opening the browser during env composing "envComposerOpenBrowser": true, - // Do you want to enable auto-answering for prompts? - // Set this field to true to skip manual confirmations. - // Configure also unknown values and prompts behavior. + // Enable auto-answering for prompts to skip manual confirmations. + // Make sure you have unknown values configured above. "skipPromptsUseAutoBehavior": false, - // Specific prompts behavior - // prompt | autoYes | autoNo + // Prompt behavior for deployment + // Options: prompt | autoYes | autoNo "deployBehavior": "prompt", "depsBehavior": "prompt", "gitBehavior": "prompt", "i18nBehavior": "prompt", "scriptsBehavior": "prompt", - // What CLI should do with existing GitHub repo - // Applicable for the new project creation only - // prompt | autoYes | autoYesSkipCommit | autoNo + // Behavior for existing GitHub repos during project creation + // Options: prompt | autoYes | autoYesSkipCommit | autoNo "existingRepoBehavior": "prompt" } diff --git a/build.config.ts b/build.config.ts index abbedf0..11492f0 100644 --- a/build.config.ts +++ b/build.config.ts @@ -3,7 +3,7 @@ import { defineBuildConfig } from "unbuild"; import pubConfig from "./pub.config.js"; export default defineBuildConfig({ - declaration: false, + declaration: true, clean: false, entries: [ { @@ -12,20 +12,13 @@ export default defineBuildConfig({ format: "esm", input: "src", ext: "js", - // pattern: [ - // "**/*.ts", - // "**/*.tsx", - // "!**/*.d.ts", - // "!**/*.test.ts", - // "!**/__tests__/**", - // ], }, ], rollup: { emitCJS: false, esbuild: { - minify: pubConfig.shouldMinify, target: "es2023", + minify: pubConfig.shouldMinify, exclude: ["**/*.test.ts", "**/__tests__/**"], }, }, diff --git a/bun.lock b/bun.lock index e01b201..0929f4b 100644 --- a/bun.lock +++ b/bun.lock @@ -22,13 +22,14 @@ "@rollup/plugin-replace": "^6.0.2", "@rollup/pluginutils": "^5.1.4", "@sinclair/typebox": "^0.34.15", + "@types/minimist": "^1.2.5", "@types/mute-stream": "^0.0.4", "@vercel/sdk": "^1.3.1", "@vitejs/plugin-react": "^4.3.4", "ai": "^4.1.16", "async-listen": "^3.0.1", "better-sqlite3": "^11.8.1", - "bun-types": "^1.2.1", + "bun-types": "^1.2.2", "c12": "^2.0.1", "citty": "^0.1.6", "cli-spinners": "^3.2.0", @@ -50,6 +51,7 @@ "jiti": "^2.4.2", "magic-regexp": "^0.8.0", "magic-string": "^0.30.17", + "minimist": "^1.2.8", "mkdist": "^2.2.0", "mlly": "^1.7.4", "mute-stream": "^2.0.0", @@ -69,7 +71,7 @@ "random-words": "^2.0.1", "react-router": "^7.1.5", "react-router-dom": "^7.1.5", - "rollup": "^4.32.1", + "rollup": "^4.34.0", "rollup-plugin-dts": "^6.1.1", "scule": "^1.3.0", "semver": "^7.7.0", @@ -104,7 +106,7 @@ "@trpc/react-query": "^11.0.0-rc.730", "@trpc/server": "^11.0.0-rc.730", "@types/better-sqlite3": "^7.6.12", - "@types/bun": "^1.2.1", + "@types/bun": "^1.2.2", "@types/cross-spawn": "^6.0.6", "@types/eslint__js": "^8.42.3", "@types/fs-extra": "^11.0.4", @@ -136,7 +138,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "superjson": "^2.2.2", - "tailwindcss": "^4.0.2", + "tailwindcss": "^4.0.3", "tsx": "^4.19.2", "type-fest": "^4.33.0", "typescript": "^5.7.3", @@ -599,43 +601,43 @@ "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.32.1", "", { "os": "android", "cpu": "arm" }, "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.0", "", { "os": "android", "cpu": "arm" }, "sha512-Eeao7ewDq79jVEsrtWIj5RNqB8p2knlm9fhR6uJ2gqP7UfbLrTrxevudVrEPDM7Wkpn/HpRC2QfazH7MXLz3vQ=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.32.1", "", { "os": "android", "cpu": "arm64" }, "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.0", "", { "os": "android", "cpu": "arm64" }, "sha512-yVh0Kf1f0Fq4tWNf6mWcbQBCLDpDrDEl88lzPgKhrgTcDrTtlmun92ywEF9dCjmYO3EFiSuJeeo9cYRxl2FswA=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.32.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gCs0ErAZ9s0Osejpc3qahTsqIPUDjSKIyxK/0BGKvL+Tn0n3Kwvj8BrCv7Y5sR1Ypz1K2qz9Ny0VvkVyoXBVUQ=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.32.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-aIB5Anc8hngk15t3GUkiO4pv42ykXHfmpXGS+CzM9CTyiWyT8HIS5ygRAy7KcFb/wiw4Br+vh1byqcHRTfq2tQ=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.32.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-kpdsUdMlVJMRMaOf/tIvxk8TQdzHhY47imwmASOuMajg/GXpw8GKNd8LNwIHE5Yd1onehNpcUB9jHY6wgw9nHQ=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.32.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-D0RDyHygOBCQiqookcPevrvgEarN0CttBecG4chOeIYCNtlKHmf5oi5kAVpXV7qs0Xh/WO2RnxeicZPtT50V0g=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.32.1", "", { "os": "linux", "cpu": "arm" }, "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mCIw8j5LPDXmCOW8mfMZwT6F/Kza03EnSr4wGYEswrEfjTfVsFOxvgYfuRMxTuUF/XmRb9WSMD5GhCWDe2iNrg=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.32.1", "", { "os": "linux", "cpu": "arm" }, "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.0", "", { "os": "linux", "cpu": "arm" }, "sha512-AwwldAu4aCJPob7zmjuDUMvvuatgs8B/QiVB0KwkUarAcPB3W+ToOT+18TQwY4z09Al7G0BvCcmLRop5zBLTag=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.32.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-e7kDUGVP+xw05pV65ZKb0zulRploU3gTu6qH1qL58PrULDGxULIS0OSDQJLH7WiFnpd3ZKUU4VM3u/Z7Zw+e7Q=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.32.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-SXYJw3zpwHgaBqTXeAZ31qfW/v50wq4HhNVvKFhRr5MnptRX2Af4KebLWR1wpxGJtLgfS2hEPuALRIY3LPAAcA=="], - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.32.1", "", { "os": "linux", "cpu": "none" }, "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw=="], + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.0", "", { "os": "linux", "cpu": "none" }, "sha512-e5XiCinINCI4RdyU3sFyBH4zzz7LiQRvHqDtRe9Dt8o/8hTBaYpdPimayF00eY2qy5j4PaaWK0azRgUench6WQ=="], - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.32.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg=="], + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3SWN3e0bAsm9ToprLFBSro8nJe6YN+5xmB11N4FfNf92wvLye/+Rh5JGQtKOpwLKt6e61R1RBc9g+luLJsc23A=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.32.1", "", { "os": "linux", "cpu": "none" }, "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.0", "", { "os": "linux", "cpu": "none" }, "sha512-B1Oqt3GLh7qmhvfnc2WQla4NuHlcxAD5LyueUi5WtMc76ZWY+6qDtQYqnxARx9r+7mDGfamD+8kTJO0pKUJeJA=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.32.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-UfUCo0h/uj48Jq2lnhX0AOhZPSTAq3Eostas+XZ+GGk22pI+Op1Y6cxQ1JkUuKYu2iU+mXj1QjPrZm9nNWV9rg=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.32.1", "", { "os": "linux", "cpu": "x64" }, "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.0", "", { "os": "linux", "cpu": "x64" }, "sha512-chZLTUIPbgcpm+Z7ALmomXW8Zh+wE2icrG+K6nt/HenPLmtwCajhQC5flNSk1Xy5EDMt/QAOz2MhzfOfJOLSiA=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.32.1", "", { "os": "linux", "cpu": "x64" }, "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.0", "", { "os": "linux", "cpu": "x64" }, "sha512-jo0UolK70O28BifvEsFD/8r25shFezl0aUk2t0VJzREWHkq19e+pcLu4kX5HiVXNz5qqkD+aAq04Ct8rkxgbyQ=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.32.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Vmg0NhAap2S54JojJchiu5An54qa6t/oKT7LmDaWggpIcaiL8WcWHEN6OQrfTdL6mQ2GFyH7j2T5/3YPEDOOGA=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.32.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-CV2aqhDDOsABKHKhNcs1SZFryffQf8vK2XrxP6lxC99ELZAdvsDgPklIBfd65R8R+qvOm1SmLaZ/Fdq961+m7A=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.32.1", "", { "os": "win32", "cpu": "x64" }, "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.0", "", { "os": "win32", "cpu": "x64" }, "sha512-g2ASy1QwHP88y5KWvblUolJz9rN+i4ZOsYzkEwcNfaNooxNUXG+ON6F5xFo0NIItpHqxcdAyls05VXpBnludGw=="], "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], @@ -709,7 +711,7 @@ "@types/better-sqlite3": ["@types/better-sqlite3@7.6.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg=="], - "@types/bun": ["@types/bun@1.2.1", "", { "dependencies": { "bun-types": "1.2.1" } }, "sha512-iiCeMAKMkft8EPQJxSbpVRD0DKqrh91w40zunNajce3nMNNFd/LnAquVisSZC+UpTMjDwtcdyzbWct08IvEqRA=="], + "@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], @@ -733,6 +735,8 @@ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/minimist": ["@types/minimist@1.2.5", "", {}, "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/mute-stream": ["@types/mute-stream@0.0.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow=="], @@ -883,7 +887,7 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "bun-types": ["bun-types@1.2.1", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-p7bmXUWmrPWxhcbFVk7oUXM5jAGt94URaoa3qf4mz43MEhNAo/ot1urzBqctgvuq7y9YxkuN51u+/qm4BiIsHw=="], + "bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -1883,7 +1887,7 @@ "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], - "rollup": ["rollup@4.32.1", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.32.1", "@rollup/rollup-android-arm64": "4.32.1", "@rollup/rollup-darwin-arm64": "4.32.1", "@rollup/rollup-darwin-x64": "4.32.1", "@rollup/rollup-freebsd-arm64": "4.32.1", "@rollup/rollup-freebsd-x64": "4.32.1", "@rollup/rollup-linux-arm-gnueabihf": "4.32.1", "@rollup/rollup-linux-arm-musleabihf": "4.32.1", "@rollup/rollup-linux-arm64-gnu": "4.32.1", "@rollup/rollup-linux-arm64-musl": "4.32.1", "@rollup/rollup-linux-loongarch64-gnu": "4.32.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.32.1", "@rollup/rollup-linux-riscv64-gnu": "4.32.1", "@rollup/rollup-linux-s390x-gnu": "4.32.1", "@rollup/rollup-linux-x64-gnu": "4.32.1", "@rollup/rollup-linux-x64-musl": "4.32.1", "@rollup/rollup-win32-arm64-msvc": "4.32.1", "@rollup/rollup-win32-ia32-msvc": "4.32.1", "@rollup/rollup-win32-x64-msvc": "4.32.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA=="], + "rollup": ["rollup@4.34.0", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.0", "@rollup/rollup-android-arm64": "4.34.0", "@rollup/rollup-darwin-arm64": "4.34.0", "@rollup/rollup-darwin-x64": "4.34.0", "@rollup/rollup-freebsd-arm64": "4.34.0", "@rollup/rollup-freebsd-x64": "4.34.0", "@rollup/rollup-linux-arm-gnueabihf": "4.34.0", "@rollup/rollup-linux-arm-musleabihf": "4.34.0", "@rollup/rollup-linux-arm64-gnu": "4.34.0", "@rollup/rollup-linux-arm64-musl": "4.34.0", "@rollup/rollup-linux-loongarch64-gnu": "4.34.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.0", "@rollup/rollup-linux-riscv64-gnu": "4.34.0", "@rollup/rollup-linux-s390x-gnu": "4.34.0", "@rollup/rollup-linux-x64-gnu": "4.34.0", "@rollup/rollup-linux-x64-musl": "4.34.0", "@rollup/rollup-win32-arm64-msvc": "4.34.0", "@rollup/rollup-win32-ia32-msvc": "4.34.0", "@rollup/rollup-win32-x64-msvc": "4.34.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-+4C/cgJ9w6sudisA0nZz0+O7lTP9a3CzNLsoDwaRumM8QHwghUsu6tqHXiTmNUp/rqNiM14++7dkzHDyCRs0Jg=="], "rollup-plugin-dts": ["rollup-plugin-dts@6.1.1", "", { "dependencies": { "magic-string": "^0.30.10" }, "optionalDependencies": { "@babel/code-frame": "^7.24.2" }, "peerDependencies": { "rollup": "^3.29.4 || ^4", "typescript": "^4.5 || ^5.0" } }, "sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA=="], @@ -2009,7 +2013,7 @@ "swr": ["swr@2.3.0", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA=="], - "tailwindcss": ["tailwindcss@4.0.2", "", {}, "sha512-cjWQjZEbzQNqH4IiSjRcYg96zjlu+rjzTFkqTc/fN3FNnIU4CoNlQvwCsIomwG/2EPWYT9ysFL1QF6Av3fZeNg=="], + "tailwindcss": ["tailwindcss@4.0.3", "", {}, "sha512-ImmZF0Lon5RrQpsEAKGxRvHwCvMgSC4XVlFRqmbzTEDb/3wvin9zfEZrMwgsa3yqBbPqahYcVI6lulM2S7IZAA=="], "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], @@ -2231,8 +2235,14 @@ "@reliverse/fs/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + "@reliverse/prompts/bun-types": ["bun-types@1.2.1", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-p7bmXUWmrPWxhcbFVk7oUXM5jAGt94URaoa3qf4mz43MEhNAo/ot1urzBqctgvuq7y9YxkuN51u+/qm4BiIsHw=="], + "@reliverse/prompts/gradient-string": ["gradient-string@3.0.0", "", { "dependencies": { "chalk": "^5.3.0", "tinygradient": "^1.1.5" } }, "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg=="], + "@reliverse/relico/bun-types": ["bun-types@1.2.1", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-p7bmXUWmrPWxhcbFVk7oUXM5jAGt94URaoa3qf4mz43MEhNAo/ot1urzBqctgvuq7y9YxkuN51u+/qm4BiIsHw=="], + + "@reliverse/runtime/bun-types": ["bun-types@1.2.1", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-p7bmXUWmrPWxhcbFVk7oUXM5jAGt94URaoa3qf4mz43MEhNAo/ot1urzBqctgvuq7y9YxkuN51u+/qm4BiIsHw=="], + "@rollup/plugin-commonjs/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], "@rollup/pluginutils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], @@ -2265,8 +2275,6 @@ "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "bun-types/@types/node": ["@types/node@22.12.0", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA=="], - "c12/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -2399,10 +2407,14 @@ "tsx/esbuild": ["esbuild@0.23.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.23.1", "@esbuild/android-arm": "0.23.1", "@esbuild/android-arm64": "0.23.1", "@esbuild/android-x64": "0.23.1", "@esbuild/darwin-arm64": "0.23.1", "@esbuild/darwin-x64": "0.23.1", "@esbuild/freebsd-arm64": "0.23.1", "@esbuild/freebsd-x64": "0.23.1", "@esbuild/linux-arm": "0.23.1", "@esbuild/linux-arm64": "0.23.1", "@esbuild/linux-ia32": "0.23.1", "@esbuild/linux-loong64": "0.23.1", "@esbuild/linux-mips64el": "0.23.1", "@esbuild/linux-ppc64": "0.23.1", "@esbuild/linux-riscv64": "0.23.1", "@esbuild/linux-s390x": "0.23.1", "@esbuild/linux-x64": "0.23.1", "@esbuild/netbsd-x64": "0.23.1", "@esbuild/openbsd-arm64": "0.23.1", "@esbuild/openbsd-x64": "0.23.1", "@esbuild/sunos-x64": "0.23.1", "@esbuild/win32-arm64": "0.23.1", "@esbuild/win32-ia32": "0.23.1", "@esbuild/win32-x64": "0.23.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg=="], + "unbuild/rollup": ["rollup@4.32.1", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.32.1", "@rollup/rollup-android-arm64": "4.32.1", "@rollup/rollup-darwin-arm64": "4.32.1", "@rollup/rollup-darwin-x64": "4.32.1", "@rollup/rollup-freebsd-arm64": "4.32.1", "@rollup/rollup-freebsd-x64": "4.32.1", "@rollup/rollup-linux-arm-gnueabihf": "4.32.1", "@rollup/rollup-linux-arm-musleabihf": "4.32.1", "@rollup/rollup-linux-arm64-gnu": "4.32.1", "@rollup/rollup-linux-arm64-musl": "4.32.1", "@rollup/rollup-linux-loongarch64-gnu": "4.32.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.32.1", "@rollup/rollup-linux-riscv64-gnu": "4.32.1", "@rollup/rollup-linux-s390x-gnu": "4.32.1", "@rollup/rollup-linux-x64-gnu": "4.32.1", "@rollup/rollup-linux-x64-musl": "4.32.1", "@rollup/rollup-win32-arm64-msvc": "4.32.1", "@rollup/rollup-win32-ia32-msvc": "4.32.1", "@rollup/rollup-win32-x64-msvc": "4.32.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA=="], + "verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], "vite/postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="], + "vite/rollup": ["rollup@4.32.1", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.32.1", "@rollup/rollup-android-arm64": "4.32.1", "@rollup/rollup-darwin-arm64": "4.32.1", "@rollup/rollup-darwin-x64": "4.32.1", "@rollup/rollup-freebsd-arm64": "4.32.1", "@rollup/rollup-freebsd-x64": "4.32.1", "@rollup/rollup-linux-arm-gnueabihf": "4.32.1", "@rollup/rollup-linux-arm-musleabihf": "4.32.1", "@rollup/rollup-linux-arm64-gnu": "4.32.1", "@rollup/rollup-linux-arm64-musl": "4.32.1", "@rollup/rollup-linux-loongarch64-gnu": "4.32.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.32.1", "@rollup/rollup-linux-riscv64-gnu": "4.32.1", "@rollup/rollup-linux-s390x-gnu": "4.32.1", "@rollup/rollup-linux-x64-gnu": "4.32.1", "@rollup/rollup-linux-x64-musl": "4.32.1", "@rollup/rollup-win32-arm64-msvc": "4.32.1", "@rollup/rollup-win32-ia32-msvc": "4.32.1", "@rollup/rollup-win32-x64-msvc": "4.32.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-z+aeEsOeEa3mEbS1Tjl6sAZ8NE3+AalQz1RJGj81M+fizusbdDMoEJwdJNHfaB40Scr4qNu+welOfes7maKonA=="], + "vite-node/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "vitest/vite-node": ["vite-node@3.0.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", "pathe": "^2.0.2", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-7JZKEzcYV2Nx3u6rlvN8qdo3QV7Fxyt6hx+CCKz9fbWxdX5IvUOmTWEAxMrWxaiSf7CKGLJQ5rFu8prb/jBjOA=="], @@ -2461,8 +2473,14 @@ "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "@reliverse/prompts/bun-types/@types/node": ["@types/node@22.12.0", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA=="], + "@reliverse/prompts/gradient-string/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + "@reliverse/relico/bun-types/@types/node": ["@types/node@22.12.0", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA=="], + + "@reliverse/runtime/bun-types/@types/node": ["@types/node@22.12.0", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "changelogen/c12/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -2603,8 +2621,84 @@ "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.23.1", "", { "os": "win32", "cpu": "x64" }, "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg=="], + "unbuild/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.32.1", "", { "os": "android", "cpu": "arm" }, "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA=="], + + "unbuild/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.32.1", "", { "os": "android", "cpu": "arm64" }, "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q=="], + + "unbuild/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.32.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA=="], + + "unbuild/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.32.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q=="], + + "unbuild/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.32.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA=="], + + "unbuild/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.32.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw=="], + + "unbuild/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.32.1", "", { "os": "linux", "cpu": "arm" }, "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g=="], + + "unbuild/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.32.1", "", { "os": "linux", "cpu": "arm" }, "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q=="], + + "unbuild/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.32.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw=="], + + "unbuild/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.32.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw=="], + + "unbuild/rollup/@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.32.1", "", { "os": "linux", "cpu": "none" }, "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw=="], + + "unbuild/rollup/@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.32.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg=="], + + "unbuild/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.32.1", "", { "os": "linux", "cpu": "none" }, "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g=="], + + "unbuild/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.32.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ=="], + + "unbuild/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.32.1", "", { "os": "linux", "cpu": "x64" }, "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg=="], + + "unbuild/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.32.1", "", { "os": "linux", "cpu": "x64" }, "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA=="], + + "unbuild/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.32.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ=="], + + "unbuild/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.32.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ=="], + + "unbuild/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.32.1", "", { "os": "win32", "cpu": "x64" }, "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q=="], + "vite/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], + "vite/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.32.1", "", { "os": "android", "cpu": "arm" }, "sha512-/pqA4DmqyCm8u5YIDzIdlLcEmuvxb0v8fZdFhVMszSpDTgbQKdw3/mB3eMUHIbubtJ6F9j+LtmyCnHTEqIHyzA=="], + + "vite/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.32.1", "", { "os": "android", "cpu": "arm64" }, "sha512-If3PDskT77q7zgqVqYuj7WG3WC08G1kwXGVFi9Jr8nY6eHucREHkfpX79c0ACAjLj3QIWKPJR7w4i+f5EdLH5Q=="], + + "vite/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.32.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zCpKHioQ9KgZToFp5Wvz6zaWbMzYQ2LJHQ+QixDKq52KKrF65ueu6Af4hLlLWHjX1Wf/0G5kSJM9PySW9IrvHA=="], + + "vite/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.32.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-sFvF+t2+TyUo/ZQqUcifrJIgznx58oFZbdHS9TvHq3xhPVL9nOp+yZ6LKrO9GWTP+6DbFtoyLDbjTpR62Mbr3Q=="], + + "vite/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.32.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-NbOa+7InvMWRcY9RG+B6kKIMD/FsnQPH0MWUvDlQB1iXnF/UcKSudCXZtv4lW+C276g3w5AxPbfry5rSYvyeYA=="], + + "vite/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.32.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JRBRmwvHPXR881j2xjry8HZ86wIPK2CcDw0EXchE1UgU0ubWp9nvlT7cZYKc6bkypBt745b4bglf3+xJ7hXWWw=="], + + "vite/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.32.1", "", { "os": "linux", "cpu": "arm" }, "sha512-PKvszb+9o/vVdUzCCjL0sKHukEQV39tD3fepXxYrHE3sTKrRdCydI7uldRLbjLmDA3TFDmh418XH19NOsDRH8g=="], + + "vite/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.32.1", "", { "os": "linux", "cpu": "arm" }, "sha512-9WHEMV6Y89eL606ReYowXuGF1Yb2vwfKWKdD1A5h+OYnPZSJvxbEjxTRKPgi7tkP2DSnW0YLab1ooy+i/FQp/Q=="], + + "vite/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.32.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-tZWc9iEt5fGJ1CL2LRPw8OttkCBDs+D8D3oEM8mH8S1ICZCtFJhD7DZ3XMGM8kpqHvhGUTvNUYVDnmkj4BDXnw=="], + + "vite/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.32.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-FTYc2YoTWUsBz5GTTgGkRYYJ5NGJIi/rCY4oK/I8aKowx1ToXeoVVbIE4LGAjsauvlhjfl0MYacxClLld1VrOw=="], + + "vite/rollup/@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.32.1", "", { "os": "linux", "cpu": "none" }, "sha512-F51qLdOtpS6P1zJVRzYM0v6MrBNypyPEN1GfMiz0gPu9jN8ScGaEFIZQwteSsGKg799oR5EaP7+B2jHgL+d+Kw=="], + + "vite/rollup/@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.32.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-wO0WkfSppfX4YFm5KhdCCpnpGbtgQNj/tgvYzrVYFKDpven8w2N6Gg5nB6w+wAMO3AIfSTWeTjfVe+uZ23zAlg=="], + + "vite/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.32.1", "", { "os": "linux", "cpu": "none" }, "sha512-iWswS9cIXfJO1MFYtI/4jjlrGb/V58oMu4dYJIKnR5UIwbkzR0PJ09O0PDZT0oJ3LYWXBSWahNf/Mjo6i1E5/g=="], + + "vite/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.32.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-RKt8NI9tebzmEthMnfVgG3i/XeECkMPS+ibVZjZ6mNekpbbUmkNWuIN2yHsb/mBPyZke4nlI4YqIdFPgKuoyQQ=="], + + "vite/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.32.1", "", { "os": "linux", "cpu": "x64" }, "sha512-WQFLZ9c42ECqEjwg/GHHsouij3pzLXkFdz0UxHa/0OM12LzvX7DzedlY0SIEly2v18YZLRhCRoHZDxbBSWoGYg=="], + + "vite/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.32.1", "", { "os": "linux", "cpu": "x64" }, "sha512-BLoiyHDOWoS3uccNSADMza6V6vCNiphi94tQlVIL5de+r6r/CCQuNnerf+1g2mnk2b6edp5dk0nhdZ7aEjOBsA=="], + + "vite/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.32.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-w2l3UnlgYTNNU+Z6wOR8YdaioqfEnwPjIsJ66KxKAf0p+AuL2FHeTX6qvM+p/Ue3XPBVNyVSfCrfZiQh7vZHLQ=="], + + "vite/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.32.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-Am9H+TGLomPGkBnaPWie4F3x+yQ2rr4Bk2jpwy+iV+Gel9jLAu/KqT8k3X4jxFPW6Zf8OMnehyutsd+eHoq1WQ=="], + + "vite/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.32.1", "", { "os": "win32", "cpu": "x64" }, "sha512-ar80GhdZb4DgmW3myIS9nRFYcpJRSME8iqWgzH2i44u+IdrzmiXVxeFnExQ5v4JYUSpg94bWjevMG8JHf1Da5Q=="], + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], diff --git a/cspell.json b/cspell.json index 954b23c..266193f 100644 --- a/cspell.json +++ b/cspell.json @@ -1,5 +1,5 @@ { - "version": "1.4.55", + "version": "1.4.60", "language": "en", "ignorePaths": [ "**/*.lock", diff --git a/package.json b/package.json index 29c4cb3..1e27511 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@reliverse/cli", "author": "reliverse", - "version": "1.4.53", + "version": "1.4.60", "type": "module", "license": "MIT", "description": "This superapp CLI tool can help you easily create new web projects, manage existing projects, and automatically make advanced codebase modifications, with more features coming soon.", @@ -42,13 +42,14 @@ "@rollup/plugin-replace": "^6.0.2", "@rollup/pluginutils": "^5.1.4", "@sinclair/typebox": "^0.34.15", + "@types/minimist": "^1.2.5", "@types/mute-stream": "^0.0.4", "@vercel/sdk": "^1.3.1", "@vitejs/plugin-react": "^4.3.4", "ai": "^4.1.16", "async-listen": "^3.0.1", "better-sqlite3": "^11.8.1", - "bun-types": "^1.2.1", + "bun-types": "^1.2.2", "c12": "^2.0.1", "citty": "^0.1.6", "cli-spinners": "^3.2.0", @@ -70,6 +71,7 @@ "jiti": "^2.4.2", "magic-regexp": "^0.8.0", "magic-string": "^0.30.17", + "minimist": "^1.2.8", "mkdist": "^2.2.0", "mlly": "^1.7.4", "mute-stream": "^2.0.0", @@ -89,7 +91,7 @@ "random-words": "^2.0.1", "react-router": "^7.1.5", "react-router-dom": "^7.1.5", - "rollup": "^4.32.1", + "rollup": "^4.34.0", "rollup-plugin-dts": "^6.1.1", "scule": "^1.3.0", "semver": "^7.7.0", @@ -124,7 +126,7 @@ "@trpc/react-query": "^11.0.0-rc.730", "@trpc/server": "^11.0.0-rc.730", "@types/better-sqlite3": "^7.6.12", - "@types/bun": "^1.2.1", + "@types/bun": "^1.2.2", "@types/cross-spawn": "^6.0.6", "@types/eslint__js": "^8.42.3", "@types/fs-extra": "^11.0.4", @@ -156,7 +158,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "superjson": "^2.2.2", - "tailwindcss": "^4.0.2", + "tailwindcss": "^4.0.3", "tsx": "^4.19.2", "type-fest": "^4.33.0", "typescript": "^5.7.3", diff --git a/pub.config.ts b/pub.config.ts index b63e7cf..ae57972 100644 --- a/pub.config.ts +++ b/pub.config.ts @@ -58,7 +58,7 @@ export type PublishConfig = { /** * The bundler to use for building the JSR-optimized package. */ - builderJsR: "bun" | "copy" | "mkdist" | "rollup" | "untyped"; + builderJsr: "bun" | "copy" | "mkdist" | "rollup" | "untyped"; /** * If `true`, minify the build output. @@ -111,8 +111,8 @@ const pubConfig: PublishConfig = { disableBump: false, // Behavior toggles - pausePublish: true, - verbose: true, + pausePublish: false, + verbose: false, dryRun: false, // Output directories @@ -122,13 +122,13 @@ const pubConfig: PublishConfig = { // Build configuration builderNpm: "mkdist", - builderJsR: "copy", + builderJsr: "copy", shouldMinify: true, splitting: false, sourcemap: "linked", + publicPath: "/", target: "node", format: "esm", - publicPath: "/", }; export default pubConfig; diff --git a/pub.setup.ts b/pub.setup.ts index 45d29d6..8f7e888 100644 --- a/pub.setup.ts +++ b/pub.setup.ts @@ -103,30 +103,47 @@ async function cleanupDistFolders() { } } +// ---------- Delete Temporary Files (with -temp postfix) ---------- +async function deleteTempFiles(dir: string) { + const patterns = ["**/*-temp.js", "**/*-temp.ts", "**/*-temp.d.ts"]; + const files = await globby(patterns, { + cwd: dir, + absolute: true, + gitignore: true, + }); + if (files.length > 0) { + await Promise.all(files.map((file) => fs.remove(file))); + logger.verbose(`Deleted temporary files:\n${files.join("\n")}`); + } +} + // ---------- Bump Versions in Files ---------- async function bumpVersions(oldVersion: string, newVersion: string) { try { - const codebase = await globby("**/*.{reliverse,json,jsonc,json5,ts,tsx}", { - ignore: [ - "**/node_modules/**", - "**/.git/**", - "**/dist/**", - "**/build/**", - "**/.next/**", - "**/coverage/**", - "**/.cache/**", - "**/tmp/**", - "**/.temp/**", - "**/package-lock.json", - "**/pnpm-lock.yaml", - "**/yarn.lock", - "**/bun.lock", - ], - }); - - const updatedFiles: string[] = []; + const codebase = await globby( + ["**/*.{reliverse,json,jsonc,json5,ts,tsx}"], + { + ignore: [ + "**/node_modules/**", + "**/.git/**", + "**/dist/**", + "**/build/**", + "**/.next/**", + "**/coverage/**", + "**/.cache/**", + "**/tmp/**", + "**/.temp/**", + "**/package-lock.json", + "**/pnpm-lock.yaml", + "**/yarn.lock", + "**/bun.lock", + ], + gitignore: true, + }, + ); - for (const file of codebase) { + // Process all files concurrently + const updateFile = async (file: string): Promise => { try { const content = await fs.readFile(file, "utf-8"); let parsed: any; @@ -160,28 +177,30 @@ async function bumpVersions(oldVersion: string, newVersion: string) { if (updatedContent) { await fs.writeFile(file, updatedContent); - updatedFiles.push(file); + return file; } else { - // If there's any plain references like "1.2.3" in .ts or .tsx - // we do a literal replacement. if (content.includes(oldVersion)) { const replaced = content.replaceAll(oldVersion, newVersion); if (replaced !== content) { await fs.writeFile(file, replaced); - updatedFiles.push(file); + return file; } } } } catch (err) { logger.warn(`Failed to process ${file}: ${String(err)}`); } - } + return null; + }; + + const results = await Promise.all(codebase.map((file) => updateFile(file))); + const updatedFiles = results.filter((f): f is string => f !== null); if (updatedFiles.length > 0) { logger.info( - `Version updated from ${oldVersion} to ${newVersion} in ${ - updatedFiles.length - } file(s):\n${updatedFiles.join("\n")}`, + `Version updated from ${oldVersion} to ${newVersion} in ${updatedFiles.length} file(s):\n${updatedFiles.join( + "\n", + )}`, ); } else { logger.warn("No files were updated with the new version."); @@ -214,11 +233,23 @@ function autoIncrementVersion( // ---------- setBumpDisabled ---------- async function setBumpDisabled(value: boolean) { - // We store the 'disableBump' value directly in pub.config.ts - const configPath = path.join(CURRENT_DIR, "pub.config.ts"); + // Try to update pub.config.ts; if it doesn't exist, fall back to pub.config.js. + const tsConfigPath = path.join(CURRENT_DIR, "pub.config.ts"); + const jsConfigPath = path.join(CURRENT_DIR, "pub.config.js"); + let configPath = tsConfigPath; + if (!(await fs.pathExists(configPath))) { + if (await fs.pathExists(jsConfigPath)) { + configPath = jsConfigPath; + } else { + logger.warn( + "No pub.config.ts or pub.config.js found to update disableBump", + ); + return; + } + } let content = await fs.readFile(configPath, "utf-8"); - // Update the disableBump value in pub.config.ts + // Update the disableBump value in the config file content = content.replace( /disableBump:\s*(true|false)/, `disableBump: ${value}`, @@ -228,7 +259,7 @@ async function setBumpDisabled(value: boolean) { // ---------- Bump Handler ---------- async function bumpHandler() { - // If disableBump or pausePublish is set, we skip version bump + // If disableBump or pausePublish is set, skip version bump if (pubConfig.disableBump || pubConfig.pausePublish) { logger.info( "Skipping version bump because a previous run already bumped the version or config paused it.", @@ -236,8 +267,8 @@ async function bumpHandler() { return; } - // If --bump= is provided, we do a direct bump to that semver. - // Otherwise, we do auto-increment based on config.bump. + // If --bump= is provided, do a direct bump to that semver. + // Otherwise, auto-increment based on config.bump. const cliVersion = scriptFlags["bump"]; const pkgPath = path.resolve("package.json"); @@ -258,7 +289,7 @@ async function bumpHandler() { } if (oldVersion !== cliVersion) { await bumpVersions(oldVersion, cliVersion); - // Mark that we have bumped (so we don't repeat if a later step fails) + // Mark that we have bumped so it isn’t repeated if a later step fails await setBumpDisabled(true); } else { logger.info(`Version is already at ${oldVersion}, no bump needed.`); @@ -303,7 +334,7 @@ type BuildConfig = { publicPath: string; disableBump: boolean; builderNpm: string; - builderJsR: string; + builderJsr: string; }; // ---------- defineConfig ---------- @@ -322,7 +353,11 @@ function defineConfig(isJSR: boolean): BuildConfig { "**/*.test.d.ts", "**/__tests__/**", "**/*.temp.js", + "**/*.temp.ts", "**/*.temp.d.ts", + "**/*-temp.js", + "**/*-temp.ts", + "**/*-temp.d.ts", // For NPM, remove .ts except .d.ts ...(isJSR ? [] : ["**/*.ts", "**/*.tsx", "!**/*.d.ts"]), ], @@ -335,29 +370,36 @@ function defineConfig(isJSR: boolean): BuildConfig { publicPath: pubConfig.publicPath, disableBump: pubConfig.disableBump, builderNpm: pubConfig.builderNpm, - builderJsR: pubConfig.builderJsR, + builderJsr: pubConfig.builderJsr, }; } // ---------- Create Common Package Fields ---------- async function createCommonPackageFields(): Promise> { const originalPkg = await readPackageJSON(); + const pkgName = originalPkg.name; + const pkgAuthor = originalPkg.author; + const pkgVersion = originalPkg.version; + const pkgLicense = originalPkg.license || "MIT"; + const pkgDescription = originalPkg.description; + const pkgHomepage = "https://docs.reliverse.org/cli"; + return { - name: originalPkg.name, - author: originalPkg.author, - version: originalPkg.version, - license: originalPkg.license, - description: originalPkg.description, - homepage: "https://docs.reliverse.org/cli", + name: pkgName, + author: pkgAuthor, + version: pkgVersion, + license: pkgLicense, + description: pkgDescription, + homepage: pkgHomepage, repository: { type: "git", - url: "git+https://github.com/reliverse/relidler.git", + url: `git+https://github.com/${pkgAuthor}/${pkgName}.git`, }, bugs: { - url: "https://github.com/reliverse/relidler/issues", + url: `https://github.com/${pkgAuthor}/${pkgName}/issues`, email: "blefnk@gmail.com", }, - keywords: ["cli", "reliverse"], + keywords: ["cli", `${pkgAuthor}`], dependencies: originalPkg.dependencies || {}, type: "module", }; @@ -500,9 +542,7 @@ async function convertJsToTsImports(dir: string, isJSR: boolean) { /(from\s*['"])([^'"]+?)\.js(?=['"])/g, "$1$2.ts", ); - logger.verbose( - `Converted .js imports to .ts for JSR build in ${filePath}`, - ); + logger.verbose("Converted .js imports to .ts for JSR build"); await fs.writeFile(filePath, finalContent, "utf8"); } else { await fs.writeFile(filePath, content, "utf8"); @@ -513,9 +553,10 @@ async function convertJsToTsImports(dir: string, isJSR: boolean) { // ---------- Rename TSX Files (JSR) ---------- async function renameTsxFiles(dir: string) { - const files = await globby("**/*.tsx", { + const files = await globby(["**/*.tsx"], { cwd: dir, absolute: true, + gitignore: true, }); await Promise.all( files.map(async (filePath) => { @@ -540,7 +581,7 @@ async function prepareJsrDistDirectory(cfg: BuildConfig) { await fs.remove(dir); logger.verbose(`Removed existing '${dir}' directory`); } - // Сopy source files into the jsr dist folder + // Copy source files into the jsr dist folder if (cfg.isJSR) { const binDir = path.join(dir, "bin"); await fs.ensureDir(binDir); @@ -552,19 +593,30 @@ async function prepareJsrDistDirectory(cfg: BuildConfig) { // ---------- Create JSR config (jsr.jsonc, etc.) ---------- async function createJsrConfig(outputDir: string) { const originalPkg = await readPackageJSON(); + const pkgAuthor = originalPkg.author; + const pkgName = originalPkg.name; + const pkgVersion = originalPkg.version; + const pkgLicense = originalPkg.license || "MIT"; + const pkgDescription = originalPkg.description; + const pkgHomepage = "https://docs.reliverse.org/cli"; + const jsrConfig = { - name: "@reliverse/relidler", - version: originalPkg.version, - author: "blefnk", - license: "MIT", + name: pkgName, + author: pkgAuthor, + version: pkgVersion, + license: pkgLicense, + description: pkgDescription, + homepage: pkgHomepage, exports: "./bin/main.ts", publish: { exclude: ["!.", "node_modules/**", ".env"], }, }; + await fs.writeJSON(path.join(outputDir, "jsr.jsonc"), jsrConfig, { spaces: 2, }); + logger.verbose("Generated jsr.jsonc file"); } @@ -610,45 +662,125 @@ async function getDirectorySize(dirPath: string): Promise { } } -// ---------- Convert `~/` Paths to Relative in bin ---------- -async function convertTildePaths(distDir: string) { +/** + * Computes a relative import path from a source file to a target sub-path inside a base directory. + * + * @param sourceFile - The absolute path of the file containing the import. + * @param subPath - The sub-path specified after the symbol prefix (e.g., 'utils/helper'). + * @param baseDir - The base directory for resolving symbol paths. + * @param prefix - An optional prefix to prepend to the computed path. + * @returns The computed relative import path with forward slashes. + */ +function getRelativeImportPath( + sourceFile: string, + subPath: string, + baseDir: string, + prefix = "", +): string { + const targetPath = path.join(baseDir, subPath); + let relativePath = path + .relative(path.dirname(sourceFile), targetPath) + .replace(/\\/g, "/"); + + if (!relativePath.startsWith(".") && !relativePath.startsWith("/")) { + relativePath = `./${relativePath}`; + } + + return prefix ? `${prefix}${relativePath}` : relativePath; +} + +/** + * Replaces symbol paths (e.g., '~/...') in the provided file content with relative paths based on a base directory. + * + * @param content - The content of the file. + * @param sourceFile - The absolute path of the file being processed. + * @param baseDir - The base directory for resolving symbol paths. + * @param symbolPrefix - The prefix used for symbol paths (default: "~/"). + * @returns An object indicating whether changes were made and the updated content. + */ +function replaceSymbolPaths( + content: string, + sourceFile: string, + baseDir: string, + symbolPrefix = "~/", +): { changed: boolean; newContent: string } { + // Escape special regex characters in symbolPrefix. + const escapedSymbol = symbolPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const symbolRegex = new RegExp(`(['"])(${escapedSymbol}[^'"]+)\\1`, "g"); + let changed = false; + + const newContent = content.replace( + symbolRegex, + (_match, quote, matchedSymbol) => { + const subPath = matchedSymbol.slice(symbolPrefix.length); // Remove the symbol prefix. + const relativeImport = getRelativeImportPath( + sourceFile, + subPath, + baseDir, + ); + changed = true; + return `${quote}${relativeImport}${quote}`; + }, + ); + + return { changed, newContent }; +} + +/** + * Converts symbol paths in JavaScript/TypeScript files within the 'bin' folder of the distribution directory. + * + * @param distDir - The distribution directory containing the 'bin' folder. + * @param symbolPrefix - The prefix used for symbol paths (default: "~/"). + */ +export async function convertSymbolPaths( + distDir: string, + symbolPrefix = "~/", +): Promise { const binDir = path.join(distDir, "bin"); if (!(await fs.pathExists(binDir))) { + logger.warn(`Directory does not exist: ${binDir}`); return; } - // Find all JS/TS-like files in bin - const files = await globby("**/*.{js,ts,jsx,tsx,mjs,cjs}", { + // Find all JS/TS files in the bin directory. + const filePatterns = ["**/*.{js,ts,jsx,tsx,mjs,cjs}"]; + const files = await globby(filePatterns, { cwd: binDir, absolute: true, + gitignore: true, }); - for (const file of files) { - const content = await fs.readFile(file, "utf8"); - - if (content.includes("~/")) { - const tildeRegex = /(['"])(~\/[^'"]+)\1/g; - let changed = false; - - const newContent = content.replace( - tildeRegex, - (_match, quote, tildePath) => { - const subPath = tildePath.slice(2); // remove '~/' - const relativePath = path.relative( - path.dirname(file), - path.join(binDir, subPath), - ); - changed = true; - return `${quote}${relativePath.replace(/\\/g, "/")}${quote}`; - }, - ); + if (files.length === 0) { + logger.info(`No matching files found in: ${binDir}`); + return; + } - if (changed) { - await fs.writeFile(file, newContent, "utf8"); - logger.verbose(`Converted '~/' paths in: ${file}`); + // Process all files concurrently. + await Promise.all( + files.map(async (file) => { + try { + const content = await fs.readFile(file, "utf8"); + + // Skip processing if the file doesn't contain the symbol prefix. + if (!content.includes(symbolPrefix)) return; + + const { changed, newContent } = replaceSymbolPaths( + content, + file, + binDir, + symbolPrefix, + ); + + if (changed) { + await fs.writeFile(file, newContent, "utf8"); + logger.verbose(`Converted symbol paths in: ${file}`); + } + } catch (error: unknown) { + const errMsg = error instanceof Error ? error.message : String(error); + logger.error(`Error processing file ${file}: ${errMsg}`); } - } - } + }), + ); } // ---------- Build Project (JSR) ---------- @@ -661,7 +793,7 @@ async function buildJsrDist() { // Prepare the output directory await prepareJsrDistDirectory(cfg); - // JSR build is just direct copy + // JSR build is just a direct copy of the source files await Promise.all([ createDistPackageJSON(dir, true), convertJsToTsImports(dir, true), @@ -675,17 +807,56 @@ async function buildJsrDist() { ]); // Convert ~/ paths to relative paths in the bin folder - await convertTildePaths(dir); + await convertSymbolPaths(dir); + + // Delete temporary files ending with -temp.{js,ts,d.ts} + await deleteTempFiles(dir); // Get size summary const size = await getDirectorySize(dir); logger.success(`Successfully created JSR distribution (${size} bytes)`); - console.log("\n"); } -async function countFiles(dir: string, ext: string): Promise { - const files = await fs.readdir(dir); - return files.filter((f) => f.endsWith(ext)).length; +/** + * Recursively counts all files within a subdirectory. + * + * @param {string} rootDir - The root directory. + * @param {string} subfolder - The subdirectory (relative to rootDir) to count files in. + * @returns {Promise} The number of files found. + */ +export async function countOutputBinFiles( + rootDir: string, + subfolder: string, +): Promise { + const targetDir = path.join(rootDir, subfolder); + let fileCount = 0; + + // If the target directory doesn't exist, return 0. + if (!(await fs.pathExists(targetDir))) { + logger.error(`Directory does not exist: ${targetDir}`); + return fileCount; + } + + /** + * Recursively traverses a directory and increments fileCount for each file found. + * + * @param {string} dir - The directory to traverse. + */ + async function traverse(dir: string) { + const entries = await fs.readdir(dir); + for (const entry of entries) { + const fullPath = path.join(dir, entry); + const stats = await fs.stat(fullPath); + if (stats.isDirectory()) { + await traverse(fullPath); + } else if (stats.isFile()) { + fileCount++; + } + } + } + + await traverse(targetDir); + return fileCount; } // ---------- Build Project (NPM) ---------- @@ -710,10 +881,8 @@ async function buildNpmDist() { if (cfg.builderNpm === "mkdist") { await execaCommand("bunx unbuild", { stdio: "inherit" }); - - logger.verbose( - `mkdist build completed with ${countFiles(outputDir, "bin")} output file(s).`, - ); + const fileCount = await countOutputBinFiles(outputDir, "bin"); + logger.verbose(`mkdist build completed with ${fileCount} output file(s).`); } else { // Use Bun Builder try { @@ -766,8 +935,11 @@ async function buildNpmDist() { ]); // Convert ~/ paths to relative paths in the bin folder - logger.info("Converting tilde paths in built files..."); - await convertTildePaths(outputDir); + logger.info("Converting symbol paths in built files..."); + await convertSymbolPaths(outputDir); + + // Delete temporary files ending with -temp.{js,ts,d.ts} + await deleteTempFiles(outputDir); // Get size summary const size = await getDirectorySize(outputDir); @@ -837,7 +1009,7 @@ async function publishToJsr(dryRun: boolean) { // ---------- Main ---------- export async function main(): Promise { try { - // 1) If we are allowed to remove dist folders, check for leftover dist + // 1) If we are allowed to remove dist folders, check for leftover dist folders if (!pubConfig.pausePublish) { if (!(await checkDistFolders())) { process.exit(1); diff --git a/schema.json b/schema.json index 264bca6..3b03642 100644 --- a/schema.json +++ b/schema.json @@ -47,6 +47,10 @@ "projectDomain": { "type": "string" }, + "projectGitService": { + "type": "string", + "enum": ["github", "gitlab", "bitbucket", "none"] + }, "projectDeployService": { "type": "string", "enum": ["vercel", "netlify", "railway", "deno", "none"] @@ -362,7 +366,15 @@ }, "projectRuntime": { "type": "string", - "enum": ["nodejs", "deno", "bun"] + "enum": [ + "bun", + "deno", + "edge-light", + "fastly", + "netlify", + "node", + "workerd" + ] }, "skipPromptsUseAutoBehavior": { "type": "boolean" @@ -401,6 +413,7 @@ "projectLicense", "projectRepository", "projectDomain", + "projectGitService", "projectDeployService", "projectPackageManager", "projectState", diff --git a/src/app/app-impl.ts b/src/app/app-impl.ts index 44b6bc8..8030e85 100644 --- a/src/app/app-impl.ts +++ b/src/app/app-impl.ts @@ -6,7 +6,7 @@ import { generate } from "random-words"; import { getMainMenuOptions } from "~/app/menu/create-project/cp-modules/cli-main-modules/cli-menu-items/getMainMenuOptions.js"; import { handleOpenProjectMenu } from "~/app/menu/create-project/cp-modules/cli-main-modules/detections/detectedProjectsMenu.js"; -import { showBunWindowsMenu } from "~/utils/bun-windows/bw-mod.js"; +import { showNativeCliMenu } from "~/utils/bun-windows/bw-mod.js"; import { detectProject } from "~/utils/reliverseConfig.js"; import type { ParamsOmitSkipPN } from "./app-types.js"; @@ -73,9 +73,9 @@ export async function app(params: ParamsOmitSkipPN) { }); } else if (mainMenuOption === "clone") { await showCloneProjectMenu({ isDev, cwd, config, memory }); - } else if (mainMenuOption === "bun-windows") { + } else if (mainMenuOption === "native-cli") { const outputDir = join(homedir(), ".reliverse", "cli"); - await showBunWindowsMenu({ outputDir }); + await showNativeCliMenu({ outputDir }); } else if (mainMenuOption === "detected-projects") { await showOpenProjectMenu({ projectName, diff --git a/src/app/app-mod.ts b/src/app/app-mod.ts index 13b8cdc..5a049e3 100644 --- a/src/app/app-mod.ts +++ b/src/app/app-mod.ts @@ -1,8 +1,8 @@ import { defineCommand } from "@reliverse/prompts"; import { authCheck } from "~/args/login/login-impl.js"; -import { handleReliverseConfig } from "~/utils/reliverseConfig.js"; -import { handleReliverseMemory } from "~/utils/reliverseMemory.js"; +import { getReliverseConfig } from "~/utils/reliverseConfig.js"; +import { getReliverseMemory } from "~/utils/reliverseMemory.js"; import { getCurrentWorkingDirectory } from "~/utils/terminalHelpers.js"; import { app } from "./app-impl.js"; @@ -23,11 +23,11 @@ export default defineCommand({ run: async ({ args }) => { const isDev = args.dev; - await showStartPrompt(isDev); + await showStartPrompt(isDev, false); const cwd = getCurrentWorkingDirectory(); - const memory = await handleReliverseMemory(); - const { config, reli } = await handleReliverseConfig(cwd, isDev); + const memory = await getReliverseMemory(); + const { config, reli } = await getReliverseConfig(cwd, isDev); await authCheck(isDev, memory, useLocalhost); await app({ cwd, isDev, config, memory, reli }); diff --git a/src/app/constants.ts b/src/app/constants.ts index 220efa3..581e8ea 100644 --- a/src/app/constants.ts +++ b/src/app/constants.ts @@ -2,7 +2,7 @@ import { re } from "@reliverse/relico"; import path from "pathe"; import { fileURLToPath } from "url"; -export const cliVersion = "1.4.55"; +export const cliVersion = "1.4.60"; export const cliName = "@reliverse/cli"; export const cliDomain = "https://docs.reliverse.org"; diff --git a/src/app/menu/create-project/cp-impl.ts b/src/app/menu/create-project/cp-impl.ts index e5210f9..cf76da3 100644 --- a/src/app/menu/create-project/cp-impl.ts +++ b/src/app/menu/create-project/cp-impl.ts @@ -176,6 +176,27 @@ export async function setupI18nSupport( return shouldEnableI18n; } +/** + * Decides whether to install deps based on config or user input. + */ +export async function shouldInstallDependencies( + behavior: Behavior, +): Promise { + if (behavior === "autoYes") return true; + if (behavior === "autoNo") return false; + + return await confirmPrompt({ + title: "Would you like to install dependencies now?", + content: + "- Recommended, but may take time.\n" + + "- Enables execution of scripts provided by the template.\n" + + "- Crucial if you've provided a fresh database API key.\n" + + "- Avoids potential Vercel build failures by ensuring `db:push` is run at least once.\n" + + "- Allows running additional necessary scripts after installation.", + defaultValue: true, + }); +} + /** * Installs dependencies and checks optional DB push script. */ @@ -184,7 +205,7 @@ export async function handleDependencies( config: ReliverseConfig, ) { const depsBehavior: Behavior = config?.depsBehavior ?? "prompt"; - const shouldInstallDeps = await determineShouldInstallDeps(depsBehavior); + const shouldInstallDeps = await shouldInstallDependencies(depsBehavior); let shouldRunDbPush = false; if (shouldInstallDeps) { @@ -202,29 +223,6 @@ export async function handleDependencies( return { shouldInstallDeps, shouldRunDbPush }; } -/** - * Decides whether to install deps based on config or user input. - */ -export async function determineShouldInstallDeps( - depsBehavior: Behavior, -): Promise { - switch (depsBehavior) { - case "autoYes": - return true; - case "autoNo": - return false; - default: { - return await confirmPrompt({ - title: - "Install dependencies now? Highly recommended, but may take time.", - content: - "This allows me to run scripts provided by the template. This is especially important if you provided me with a fresh database API key—you may experience Vercel build failures if you don't run db:push at least once. I can run this script after installing dependencies.", - defaultValue: true, - }); - } - } -} - /** * Moves the project from a test runtime directory to a user-specified location. */ diff --git a/src/app/menu/create-project/cp-mod.ts b/src/app/menu/create-project/cp-mod.ts index 04774c4..50aae8f 100644 --- a/src/app/menu/create-project/cp-mod.ts +++ b/src/app/menu/create-project/cp-mod.ts @@ -75,6 +75,8 @@ export async function createWebProject({ projectPath: "", projectName, selectedRepo, + config, + preserveGit: false, }); // ------------------------------------------------- diff --git a/src/app/menu/create-project/cp-modules/cli-main-modules/cli-menu-items/getMainMenuOptions.ts b/src/app/menu/create-project/cp-modules/cli-main-modules/cli-menu-items/getMainMenuOptions.ts index 543cb58..d918d4a 100644 --- a/src/app/menu/create-project/cp-modules/cli-main-modules/cli-menu-items/getMainMenuOptions.ts +++ b/src/app/menu/create-project/cp-modules/cli-main-modules/cli-menu-items/getMainMenuOptions.ts @@ -1,6 +1,7 @@ import { re } from "@reliverse/relico"; -import { isWindows, isBunPM } from "@reliverse/runtime"; +import { isBunPM, isBunRuntime } from "@reliverse/runtime"; import fs from "fs-extra"; +import { homedir } from "node:os"; import path from "pathe"; import type { ReliverseConfig } from "~/utils/schemaConfig.js"; @@ -12,7 +13,7 @@ export type MainMenuChoice = | "clone" | "detected-projects" | "isDevTools" - | "bun-windows" + | "native-cli" | "exit"; type MainMenuOption = { @@ -26,8 +27,6 @@ export async function getMainMenuOptions( isDev: boolean, reli: ReliverseConfig[], ): Promise { - // Note: The blank line issue is not in this file, but rather in the selectPrompt implementation - // where deleteLastLine() is called before completePrompt() const multiConfigMsg = reli.length > 0 ? re.dim(`multi-config mode with ${reli.length} projects`) @@ -47,20 +46,27 @@ export async function getMainMenuOptions( }, ]; - // 2) Conditionally add the dev tools option - if (isDev) { - options.push({ - label: "🧰 Open developer tools", - value: "isDevTools", - }); - } + // 2) Inject the dev tools option + options.push({ + label: "🧰 Open developer tools", + value: "isDevTools", + }); + + // 3) Inject native-cli option if using Bun PM + // TODO: possibly deprecated, bun has fixed runtime issue on windows + if (isBunPM && !isBunRuntime) { + const isNativeInstalled = await fs.pathExists( + path.join(homedir(), ".reliverse", "cli"), + ); + + let msg = "Use"; + if (isNativeInstalled && isBunRuntime) { + msg = "Configure"; + } - // 3) Add bun-windows option if on Windows with Bun PM - if (isWindows && isBunPM) { options.push({ - label: "🚀 Use Bun-native @reliverse/cli", - value: "bun-windows", - // hint: re.dim("Setup Bun runtime"), + label: `🚀 ${msg} Bun-native @reliverse/cli`, + value: "native-cli", }); } diff --git a/src/app/menu/create-project/cp-modules/cli-main-modules/configs/env.test.ts b/src/app/menu/create-project/cp-modules/cli-main-modules/configs/env.test.ts index 19a9638..93d2f0c 100644 --- a/src/app/menu/create-project/cp-modules/cli-main-modules/configs/env.test.ts +++ b/src/app/menu/create-project/cp-modules/cli-main-modules/configs/env.test.ts @@ -9,8 +9,9 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; import fs from "fs-extra"; import path from "pathe"; +import type { ReliverseConfig } from "~/utils/schemaConfig.js"; + import { composeEnvFile } from "~/app/menu/create-project/cp-modules/compose-env-file/cef-mod.js"; -import { getReliverseConfig } from "~/utils/reliverseConfig.js"; // Mock the prompts const mockSelectPrompt = mock(selectPrompt) as Mock< @@ -49,13 +50,13 @@ describe("composeEnvFile", () => { const ENV_PATH = path.join(TEST_DIR, ".env"); const ENV_EXAMPLE_PATH = path.join(TEST_DIR, ".env.example"); let envContent: string[] = []; - let config: Awaited>; + let config: null | ReliverseConfig = null; - beforeEach(async () => { + beforeEach(() => { // Reset mocks mock.restore(); envContent = []; - config = await getReliverseConfig(); + config = null; // Setup default mock implementations const fsModule = fs as unknown as MockFsModule; diff --git a/src/app/menu/create-project/cp-modules/cli-main-modules/modules/showStartEndPrompt.ts b/src/app/menu/create-project/cp-modules/cli-main-modules/modules/showStartEndPrompt.ts index 719a88f..d06074a 100644 --- a/src/app/menu/create-project/cp-modules/cli-main-modules/modules/showStartEndPrompt.ts +++ b/src/app/menu/create-project/cp-modules/cli-main-modules/modules/showStartEndPrompt.ts @@ -4,7 +4,10 @@ import { isBun, isBunPM, isBunRuntime } from "@reliverse/runtime"; import { cliName, cliVersion } from "~/app/constants.js"; -export async function showStartPrompt(isDev: boolean) { +export async function showStartPrompt( + isDev: boolean, + showRuntimeInfo: boolean, +) { await startPrompt({ titleColor: "inverse", clearConsole: true, @@ -12,9 +15,12 @@ export async function showStartPrompt(isDev: boolean) { packageVersion: cliVersion, isDev, }); - console.log(isBunRuntime()); - console.log(await isBunPM()); - console.log(isBun); + + if (showRuntimeInfo) { + console.log("isBunRuntime:", isBunRuntime()); + console.log("isBunPM:", await isBunPM()); + console.log("isBun:", isBun); + } } export async function showEndPrompt() { diff --git a/src/app/menu/create-project/cp-modules/compose-env-file/cef-mod.ts b/src/app/menu/create-project/cp-modules/compose-env-file/cef-mod.ts index 19be21f..76f38ae 100644 --- a/src/app/menu/create-project/cp-modules/compose-env-file/cef-mod.ts +++ b/src/app/menu/create-project/cp-modules/compose-env-file/cef-mod.ts @@ -22,8 +22,10 @@ export async function composeEnvFile( fallbackEnvExampleURL: string, shouldMaskSecretInput: boolean, skipPrompts: boolean, - config: ReliverseConfig, + config: ReliverseConfig | null, ): Promise { + if (config === null) return; + try { const results = await Promise.all([ ensureExampleExists(projectDir, fallbackEnvExampleURL), diff --git a/src/app/menu/create-project/cp-modules/git-deploy-prompts/vercel/vercel-team.ts b/src/app/menu/create-project/cp-modules/git-deploy-prompts/vercel/vercel-team.ts index 6e6ef85..be32381 100644 --- a/src/app/menu/create-project/cp-modules/git-deploy-prompts/vercel/vercel-team.ts +++ b/src/app/menu/create-project/cp-modules/git-deploy-prompts/vercel/vercel-team.ts @@ -5,7 +5,7 @@ import { relinka } from "@reliverse/prompts"; import { teamsGetTeam } from "@vercel/sdk/funcs/teamsGetTeam"; import { teamsGetTeams } from "@vercel/sdk/funcs/teamsGetTeams"; -import { handleReliverseMemory } from "~/utils/reliverseMemory.js"; +import { getReliverseMemory } from "~/utils/reliverseMemory.js"; export type VercelTeam = { id: string; @@ -49,7 +49,7 @@ export async function getPrimaryVercelTeam( memory.vercelTeamSlug = team.slug; // Re-read memory to ensure changes are persisted - await handleReliverseMemory(); + await getReliverseMemory(); return team; } diff --git a/src/app/menu/create-project/cp-modules/use-composer-mode/template/base/pkg.json b/src/app/menu/create-project/cp-modules/use-composer-mode/template/base/pkg.json index 5292c62..f4069df 100644 --- a/src/app/menu/create-project/cp-modules/use-composer-mode/template/base/pkg.json +++ b/src/app/menu/create-project/cp-modules/use-composer-mode/template/base/pkg.json @@ -1,6 +1,6 @@ { "name": "template", - "version": "1.4.55", + "version": "1.4.60", "type": "module", "private": true, "scripts": { diff --git a/src/app/menu/menu-mod.ts b/src/app/menu/menu-mod.ts index 1b9a008..0081f57 100644 --- a/src/app/menu/menu-mod.ts +++ b/src/app/menu/menu-mod.ts @@ -158,37 +158,59 @@ export async function showDevToolsMenu(params: ParamsOmitReli) { hasVercelToken = true; } + const toolsOptions = { + rmTestsRuntime: "rm-tests-runtime", + downloadTemplate: "download-template", + openVercelDevtools: "open-vercel-devtools", + reReadReliverse: "re-read-reliverse", + aiChatTest: "ai-chat-test", + exit: "exit", + } as const; + const option = await selectPrompt({ title: "Dev tools menu", options: [ - ...(TestsRuntimeExists - ? [{ label: "remove tests-runtime dir", value: "rm-tests-runtime" }] + ...(isDev && TestsRuntimeExists + ? [ + { + label: "remove tests-runtime dir", + value: toolsOptions.rmTestsRuntime, + }, + ] + : []), + ...(isDev + ? [ + { + label: + "downloadRepo + cd(tests-runtime) + composeEnvFile + promptGitDeploy", + value: toolsOptions.downloadTemplate, + }, + ] : []), - { - label: - "downloadRepo + cd(tests-runtime) + composeEnvFile + promptGitDeploy", - value: "download-template", - }, ...(hasVercelToken ? [ { label: `Open Vercel devtools ${experimental}`, - value: "open-vercel-devtools", + value: toolsOptions.openVercelDevtools, + }, + ] + : []), + ...(isDev + ? [ + { + label: `Re-read config and memory ${experimental}`, + value: toolsOptions.reReadReliverse, }, ] : []), - { - label: `Re-read config and memory ${experimental}`, - value: "re-read-reliverse", - }, - { label: "Test chat with Reliverse AI", value: "ai-chat-test" }, - { label: "👈 Exit", value: "exit" }, + { label: "Test chat with Reliverse AI", value: toolsOptions.aiChatTest }, + { label: "👈 Exit", value: toolsOptions.exit }, ], }); - if (option === "rm-tests-runtime") { + if (option === toolsOptions.rmTestsRuntime) { await rmTestsRuntime(cwd); - } else if (option === "download-template") { + } else if (option === toolsOptions.downloadTemplate) { await downloadRepoOption( "blefnk/relivator", config, @@ -197,12 +219,12 @@ export async function showDevToolsMenu(params: ParamsOmitReli) { cwd, skipPrompts, ); - } else if (option === "re-read-reliverse") { + } else if (option === toolsOptions.reReadReliverse) { await reReadReliverseConfig(); await reReadReliverseMemory(); - } else if (option === "ai-chat-test") { + } else if (option === toolsOptions.aiChatTest) { await aiChatHandler(memory); - } else if (option === "open-vercel-devtools") { + } else if (option === toolsOptions.openVercelDevtools) { await openVercelDevtools(memory, vercelToken); } } diff --git a/src/args/login/login-impl.ts b/src/args/login/login-impl.ts index 88e8932..0f4e9ca 100644 --- a/src/args/login/login-impl.ts +++ b/src/args/login/login-impl.ts @@ -19,7 +19,7 @@ import type { ReliverseMemory } from "~/utils/schemaMemory.js"; import { MEMORY_FILE } from "~/app/constants.js"; import { showAnykeyPrompt } from "~/app/menu/create-project/cp-modules/cli-main-modules/modules/showAnykeyPrompt.js"; import { - handleReliverseMemory, + getReliverseMemory, updateReliverseMemory, } from "~/utils/reliverseMemory.js"; @@ -160,7 +160,7 @@ export async function auth({ process.stdout.write("\x1b[2K\r"); // Clear the current line, so misplacement of "Waiting for user confirmation..." is overwritten relinka( "info", - "The following URL will be opened in your default browser:", + "The following URL will be opened in your default browser (use Ctrl+Click to open):", confirmationUrl.toString(), ); @@ -274,7 +274,7 @@ export async function authCheck( await auth({ isDev, useLocalhost }); // Re-check authentication after auth flow - const updatedMemory = await handleReliverseMemory(); + const updatedMemory = await getReliverseMemory(); const authSuccess = updatedMemory.code && diff --git a/src/args/login/login-mod.ts b/src/args/login/login-mod.ts index 9611b85..eeacd97 100644 --- a/src/args/login/login-mod.ts +++ b/src/args/login/login-mod.ts @@ -3,7 +3,7 @@ import { relinka } from "@reliverse/prompts"; import { useLocalhost } from "~/app/constants.js"; import { showAnykeyPrompt } from "~/app/menu/create-project/cp-modules/cli-main-modules/modules/showAnykeyPrompt.js"; -import { handleReliverseMemory } from "~/utils/reliverseMemory.js"; +import { getReliverseMemory } from "~/utils/reliverseMemory.js"; import { auth } from "./login-impl.js"; @@ -23,7 +23,7 @@ export default defineCommand({ const isDev = args.dev; // Check for existing keys in SQLite - const memory = await handleReliverseMemory(); + const memory = await getReliverseMemory(); const isAuthenticated = memory.code && memory.key; if (isAuthenticated) { diff --git a/src/args/memory/memory-mod.ts b/src/args/memory/memory-mod.ts index 7ea8c79..fff6904 100644 --- a/src/args/memory/memory-mod.ts +++ b/src/args/memory/memory-mod.ts @@ -1,7 +1,7 @@ import { defineCommand } from "@reliverse/prompts"; import { relinka } from "@reliverse/prompts"; -import { handleReliverseMemory } from "~/utils/reliverseMemory.js"; +import { getReliverseMemory } from "~/utils/reliverseMemory.js"; export default defineCommand({ meta: { @@ -10,7 +10,7 @@ export default defineCommand({ hidden: true, }, run: async () => { - const memory = await handleReliverseMemory(); + const memory = await getReliverseMemory(); relinka("info", "Current memory values:"); console.log({ code: memory.code === "" ? "" : "exists", diff --git a/src/utils/bun-windows/bw-impl.ts b/src/utils/bun-windows/bw-impl.ts index 64d3cfb..4dd41dd 100644 --- a/src/utils/bun-windows/bw-impl.ts +++ b/src/utils/bun-windows/bw-impl.ts @@ -2,6 +2,7 @@ import { relinka } from "@reliverse/prompts"; import fs from "fs-extra"; import { globby } from "globby"; import fetch from "node-fetch-native"; +import { installDependencies } from "nypm"; import pLimit from "p-limit"; import { dirname, join } from "pathe"; import semver from "semver"; @@ -214,6 +215,8 @@ export async function downloadJsrDist( pkgIsCLI = true, msgDownloadStarted?: string, revertTsxFiles = false, + cliInstallDeps = true, + // cliUseExistentNodeModules = true, ): Promise { try { // 1) Get package metadata @@ -273,11 +276,20 @@ export async function downloadJsrDist( // 8) If pkgIsCLI and revertTsxFiles is true, rename -tsx.txt files back to .tsx if (pkgIsCLI && revertTsxFiles) { - relinka("info", "Reverting .tsx files..."); + relinka("info-verbose", "Reverting .tsx files..."); await renameTxtToTsx(outputDir); } - // 9) Notify user that the download is complete + // 9) If installDeps is true, install dependencies + if (pkgIsCLI && cliInstallDeps) { + relinka("info", "Installing dependencies..."); + await installDependencies({ + cwd: outputDir, + silent: false, + }); + } + + // 10) Notify user that the download is complete relinka( "success", `All files for @${scope}/${packageName} downloaded successfully.`, diff --git a/src/utils/bun-windows/bw-mod.ts b/src/utils/bun-windows/bw-mod.ts index cdd28b5..bb1f100 100644 --- a/src/utils/bun-windows/bw-mod.ts +++ b/src/utils/bun-windows/bw-mod.ts @@ -3,7 +3,7 @@ import fs from "fs-extra"; import { downloadJsrDist } from "./bw-impl.js"; -export async function showBunWindowsMenu({ outputDir }: { outputDir: string }) { +export async function showNativeCliMenu({ outputDir }: { outputDir: string }) { // @see https://jsr.io/@reliverse/cli // Check if output directory exists and is not empty @@ -31,7 +31,7 @@ export async function showBunWindowsMenu({ outputDir }: { outputDir: string }) { const shouldUseBunRuntime = await confirmPrompt({ title: - "I see you're using Windows and have Bun installed, but the process was run with the Node.js runtime. Do you want to use the Bun runtime?", + "I see you have Bun installed, but the process was run using the Node.js runtime. Do you want to use the Bun runtime?", content: "Press to allow me to download the CLI from JSR and install it globally. (The download speed depends on your internet connection.)", defaultValue: true, @@ -41,6 +41,13 @@ export async function showBunWindowsMenu({ outputDir }: { outputDir: string }) { return; } + // const shouldInstallDeps = await confirmPrompt({ + // title: "Do you want to install dependencies for the CLI?", + // defaultValue: true, + // }); + + // TODO: on reinstall, if 'cli' folder exists, we should check if node_modules exists, move it to temp dir and just move to updated folder, finally with using `bun install` + await downloadJsrDist( "reliverse", "cli", @@ -50,5 +57,7 @@ export async function showBunWindowsMenu({ outputDir }: { outputDir: string }) { 5, true, "Downloading Bun-native Reliverse CLI from JSR...", + true, + true, ); } diff --git a/src/utils/downloading/downloadRepo.ts b/src/utils/downloading/downloadRepo.ts index 380543d..d8620e9 100644 --- a/src/utils/downloading/downloadRepo.ts +++ b/src/utils/downloading/downloadRepo.ts @@ -18,63 +18,69 @@ type DownloadRepoOptions = { * The repository to download, such as "owner/repo" (github:owner/repo), "owner/repo#ref" (github:owner/repo#ref), or "github:owner/repo/subdir". */ repoURL: string; - /** * The name of the new local project directory. */ projectName: string; - /** * Indicates if this operation is running in a development environment (used to place project in a 'tests-runtime' folder). */ isDev: boolean; - /** * The current working directory where the new project folder will be created. */ cwd: string; - /** * Optional authentication token (e.g., GitHub personal access token) if the repository is private. */ auth?: string; - /** * If true, automatically installs dependencies (via `nypm`) after downloading. */ install?: boolean; - /** * Specifies which Git hosting service to use. Supported values are "github", "gitlab", "bitbucket", and "sourcehut". */ provider?: "github" | "gitlab" | "bitbucket" | "sourcehut"; - /** * If set, only extracts a specified subdirectory from the downloaded repository. */ subdirectory?: string; - /** * If true, forces the download to proceed even if the target directory is not empty. */ force?: boolean; - /** * If true, removes any existing contents in the target directory before downloading. */ forceClean?: boolean; - /** * If true, preserves the .git directory when cloning, maintaining Git history. * @default true */ preserveGit?: boolean; - /** * Configuration for the project when initializing a fresh Git repository. * Only used when preserveGit is false. */ config?: ReliverseConfig | undefined; + /** + * If true, returns the duration (in seconds) it took to complete the download. + */ + returnTime?: boolean; + /** + * If true, returns the total size (in MB) of the downloaded project folder. + */ + returnSize?: boolean; + /** + * If true, returns the number of concurrent Git processes used. + */ + returnConcurrency?: boolean; + /** + * If provided, use the fast clone method. + * This should be the local path to a pre-populated ".git" folder that contains the complete history. + */ + fastCloneSource?: string; }; /** @@ -89,33 +95,65 @@ type RepoInfo = { headers?: Record; }; +type GitProvider = "github" | "gitlab" | "bitbucket" | "sourcehut"; + /** - * Represents the result of a successful download operation, including the source URL and the local directory path. + * Represents the result of a successful download operation, including the source URL, + * the local directory path, and optionally the duration, size, and concurrency. */ export type DownloadResult = { source: string; dir: string; + /** + * The duration (in seconds) it took to complete the download. + */ + time?: number; + /** + * The total size (in MB) of the downloaded project. + */ + size?: number; + /** + * The number of concurrent Git processes used. + */ + concurrency?: number; }; -type GitProvider = "github" | "gitlab" | "bitbucket" | "sourcehut"; - -/* ----------------------------------------------------------------------- - * Helper methods for URL parsing (restored) and building final RepoInfo - * ----------------------------------------------------------------------- */ +/** + * Recursively calculates the total size of a folder in bytes. + * Optionally, directories with a basename found in skipDirs will be skipped. + */ +async function getFolderSize( + directory: string, + skipDirs: string[] = [], +): Promise { + let totalSize = 0; + const entries = await fs.readdir(directory); + for (const entry of entries) { + // Skip directories that match one of the names in skipDirs. + if (skipDirs.includes(entry)) continue; + + const fullPath = path.join(directory, entry); + const stats = await fs.stat(fullPath); + if (stats.isFile()) { + totalSize += stats.size; + } else if (stats.isDirectory()) { + totalSize += await getFolderSize(fullPath, skipDirs); + } + } + return totalSize; +} /** - * Reads a Git repository string and extracts the provider (optional), repository path, reference/branch, and subdirectory. + * Reads a Git repository string and extracts the provider (if any), repository path, reference/branch, and subdirectory. * - * Supports formats: + * Supports formats such as: * - "owner/repo" * - "owner/repo#ref" * - "provider:owner/repo" * - "provider:owner/repo#ref" - * - Any of above with optional "/subdir" at end * - Full URLs (e.g., "https://github.com/owner/repo") */ function parseGitURI(input: string) { - // Normalize input: trim whitespace and remove protocol/domain if present const normalizedInput = input .trim() .replace( @@ -123,13 +161,11 @@ function parseGitURI(input: string) { "", ) .replace(/^(github|gitlab|bitbucket|sourcehut)\.com\//, "") - .replace(/^https?:\/\/git\.sr\.ht\/~/, "") // Special case for sourcehut - .replace(/^git\.sr\.ht\/~/, ""); // Special case for sourcehut without protocol - + .replace(/^https?:\/\/git\.sr\.ht\/~/, "") + .replace(/^git\.sr\.ht\/~/, ""); const pattern = /^(?:(?[^:]+):)?(?[^#]+)(?#[^/]+)?(?\/.*)?$/; const match = pattern.exec(normalizedInput); - if (!match?.groups) { return { provider: undefined, @@ -138,7 +174,6 @@ function parseGitURI(input: string) { subdir: "", }; } - const { provider, repo, refPart, subdir } = match.groups; return { provider: provider?.trim(), @@ -165,8 +200,8 @@ function getRepoUrl(repo: string, provider: GitProvider): string { } /** - * Creates a final RepoInfo object (including `gitUrl` and optional `headers`) - * from the raw repo string and user options (e.g., `auth`, `subdirectory`). + * Creates a final RepoInfo object (including gitUrl and optional headers) + * from the raw repo string and user options (e.g., auth, subdirectory). */ function computeRepoInfo( input: string, @@ -176,19 +211,12 @@ function computeRepoInfo( ): RepoInfo { const { provider: parsedProvider, repo, ref, subdir } = parseGitURI(input); const actualProvider = (parsedProvider ?? defaultProvider) as GitProvider; - - // Generate a name from the repo segment (owner/repo => owner-repo) const name = repo.replace("/", "-"); - - // Build any HTTP headers needed (e.g., Authorization) const headers: Record = {}; if (auth) { headers["Authorization"] = `Bearer ${auth}`; } - - // Build the final repository URL const gitUrl = getRepoUrl(repo, actualProvider); - return { name, version: ref, @@ -202,7 +230,6 @@ function computeRepoInfo( /** * Generates a new project name with an iteration number if the directory already exists. - * For example: "my-project" -> "my-project-1" -> "my-project-2" */ async function getUniqueProjectPath( basePath: string, @@ -212,7 +239,6 @@ async function getUniqueProjectPath( let iteration = 1; let currentPath = basePath; let currentName = projectName; - while (await fs.pathExists(currentPath)) { currentName = `${projectName}-${iteration}`; currentPath = isDev @@ -220,18 +246,20 @@ async function getUniqueProjectPath( : path.join(dirname(basePath), currentName); iteration++; } - return currentPath; } -/* ----------------------------------------------------------------------- - * Main download function (no cache usage) - * ----------------------------------------------------------------------- */ - /** * Downloads a repository using git clone (shallow clone) and optionally installs dependencies. - * If a subdirectory is requested, clones to a temporary location, then copies just that folder. - * If `preserveGit` is false, removes the .git directory or filters it out. + * If a subdirectory is requested and preserveGit is true, a sparse checkout is used so that the + * final project directory contains just that subdirectory’s content (with Git history preserved). + * Otherwise, a temporary clone is used to extract only the desired subdirectory. + * If the fastCloneSource option is provided, the method copies the pre-populated ".git" folder from + * that source and runs "git checkout -- ." to quickly rebuild the working tree while preserving complete history. + * If the corresponding return options are enabled, the returned result will include: + * - time: the duration (in seconds) it took to complete the download, + * - size: the total size (in MB) of the downloaded project (excluding the .git folder when preserveGit is false), + * - concurrency: the max number of concurrent Git processes used. */ export async function downloadRepo({ repoURL, @@ -246,15 +274,21 @@ export async function downloadRepo({ forceClean = false, preserveGit = true, config, + returnTime = false, + returnSize = false, + returnConcurrency = false, + fastCloneSource, }: DownloadRepoOptions): Promise { relinka("info-verbose", `Downloading repo ${repoURL}...`); + const startTime = Date.now(); + let tempCloneDir: string | undefined = undefined; + const maxConcurrentProcesses = 6; try { // 1) Decide where to create the project let projectPath = isDev ? path.join(cwd, "tests-runtime", projectName) : path.join(cwd, projectName); - relinka("info-verbose", `Preparing to place repo in: ${projectPath}`); // 2) Handle existing directory @@ -264,9 +298,7 @@ export async function downloadRepo({ const files = await fs.readdir(projectPath); const hasOnlyReliverseConfig = files.length === 1 && files[0] === ".reliverse"; - if (files.length > 0 && !hasOnlyReliverseConfig) { - // Instead of throwing an error, get a new unique path projectPath = await getUniqueProjectPath( projectPath, projectName, @@ -278,7 +310,6 @@ export async function downloadRepo({ ); } } - await fs.ensureDir(projectPath); // 3) Handle .reliverse file @@ -287,9 +318,7 @@ export async function downloadRepo({ const hasReliverseConfig = await fs.pathExists( path.join(projectPath, ".reliverse"), ); - if (hasReliverseConfig) { - // If there's already a .reliverse in the parent dir, ask the user how to proceed if (await fs.pathExists(tempReliverseConfigPath)) { const choice = await selectPrompt({ title: @@ -299,7 +328,6 @@ export async function downloadRepo({ { value: "backup", label: "Create backup" }, ], }); - if (choice === "delete") { await fs.remove(tempReliverseConfigPath); } else { @@ -312,8 +340,6 @@ export async function downloadRepo({ await fs.move(tempReliverseConfigPath, backupPath); } } - - // Move the .reliverse out of the target folder so we can safely remove/copy await fs.move( path.join(projectPath, ".reliverse"), tempReliverseConfigPath, @@ -324,83 +350,123 @@ export async function downloadRepo({ // 4) Parse and compute final repo info const repoInfo = computeRepoInfo(repoURL, provider, auth, subdirectory); - if (!repoInfo.gitUrl) { throw new Error(`Invalid repository URL or provider: ${repoURL}`); } - // 5) Prepare final shallow clone options - const git = simpleGit(); - // Only do a shallow clone if we're not preserving Git history - const cloneOptions = ["--branch", repoInfo.version]; - if (!preserveGit) { - cloneOptions.push("--depth", "1"); - } - - // If private repo with auth, embed token in the URL + // 5) Prepare final URL (embed auth token if needed) let finalUrl = repoInfo.gitUrl; if (auth) { const authUrl = new URL(repoInfo.gitUrl); - // For GitHub (and other providers), set username='oauth2' and password=token authUrl.username = "oauth2"; authUrl.password = auth; finalUrl = authUrl.toString(); } - // If a subdirectory was specified, we clone into a temporary directory first - let cloneTarget = projectPath; - if (repoInfo.subdir) { - cloneTarget = await fs.mkdtemp(path.join(parentDir, "gitclone-")); - } - - // Perform the clone - await git.clone(finalUrl, cloneTarget, cloneOptions); - - // 6) If subdirectory was specified, copy just that folder to the final location - if (repoInfo.subdir) { - const srcSubdir = path.join(cloneTarget, repoInfo.subdir); - if (!(await fs.pathExists(srcSubdir))) { - throw new Error( - `Subdirectory '${repoInfo.subdir}' not found in repository ${repoURL}`, - ); - } - - if (preserveGit) { - // Copy everything, including .git, from that subdir - await fs.copy(srcSubdir, projectPath); - } else { - // Copy everything except .git - await fs.copy(srcSubdir, projectPath, { - filter: (src) => !src.includes(`${path.sep}.git`), - }); - } - - // Clean up the temporary clone - await fs.remove(cloneTarget); + // 6) Clone or fast clone the repository + if (fastCloneSource) { + // --- Fast clone method --- + // Copy the pre-populated ".git" folder from fastCloneSource into projectPath + relinka( + "info-verbose", + `Using fast clone method from: ${fastCloneSource}`, + ); + await fs.copy(fastCloneSource, path.join(projectPath, ".git")); + const git = simpleGit({ maxConcurrentProcesses }); + await git.cwd(projectPath); + // Rebuild the working tree using the complete history from the .git folder + await git.checkout(["--", "."]); } else { - // If preserveGit is false, remove the .git directory - if (!preserveGit) { - await fs.remove(path.join(projectPath, ".git")); + // --- Normal clone method --- + const git = simpleGit({ maxConcurrentProcesses }); + try { + if (repoInfo.subdir) { + // A subdirectory was requested + if (preserveGit) { + // Preserve Git history: use sparse-checkout to check out only the subdirectory. + await git.clone(finalUrl, projectPath, [ + "--branch", + repoInfo.version, + ]); + await git.cwd(projectPath); + await git.raw(["sparse-checkout", "init", "--cone"]); + await git.raw(["sparse-checkout", "set", repoInfo.subdir]); + const subdirPath = path.join(projectPath, repoInfo.subdir); + if (!(await fs.pathExists(subdirPath))) { + throw new Error( + `Subdirectory '${repoInfo.subdir}' not found in repository ${repoURL}`, + ); + } + const files = await fs.readdir(subdirPath); + for (const file of files) { + await fs.move( + path.join(subdirPath, file), + path.join(projectPath, file), + { overwrite: true }, + ); + } + await fs.remove(subdirPath); + } else { + // Not preserving Git: clone the entire repository shallowly into a temporary directory, + // then copy only the requested subdirectory (excluding any .git files). + tempCloneDir = await fs.mkdtemp(path.join(parentDir, "gitclone-")); + await git.clone(finalUrl, tempCloneDir, [ + "--branch", + repoInfo.version, + "--depth", + "1", + "--single-branch", + ]); + const srcSubdir = path.join(tempCloneDir, repoInfo.subdir); + if (!(await fs.pathExists(srcSubdir))) { + throw new Error( + `Subdirectory '${repoInfo.subdir}' not found in repository ${repoURL}`, + ); + } + await fs.copy(srcSubdir, projectPath, { + filter: (src) => !src.includes(`${path.sep}.git`), + }); + } + } else { + // No subdirectory requested: do a normal clone + const cloneOptions = ["--branch", repoInfo.version]; + if (!preserveGit) { + cloneOptions.push("--depth", "1", "--single-branch"); + } + await git.clone(finalUrl, projectPath, cloneOptions); + } - // Optionally initialize a fresh Git repo - if (config) { - await initGitDir({ - cwd, - isDev, - projectName, - projectPath, - allowReInit: true, - createCommit: true, - config, - }); + // 7) Post-clone adjustments + if (!repoInfo.subdir) { + if (!preserveGit) { + await fs.remove(path.join(projectPath, ".git")); + if (config) { + await initGitDir({ + cwd, + isDev, + projectName, + projectPath, + allowReInit: true, + createCommit: true, + config, + }); + } + } else { + await setHiddenAttributeOnWindows(path.join(projectPath, ".git")); + } + } else { + if (preserveGit) { + await setHiddenAttributeOnWindows(path.join(projectPath, ".git")); + } + } + } finally { + if (tempCloneDir && (await fs.pathExists(tempCloneDir))) { + await fs.remove(tempCloneDir); } - } else { - // If preserveGit is true, hide .git folder on Windows - await setHiddenAttributeOnWindows(path.join(projectPath, ".git")); } } - // 7) Restore .reliverse if it was moved + // 8) Restore .reliverse if it was moved if (hasReliverseConfig) { await fs.move( tempReliverseConfigPath, @@ -409,7 +475,7 @@ export async function downloadRepo({ ); } - // 8) Install dependencies if requested + // 9) Install dependencies if requested if (install) { relinka("info", "Installing dependencies..."); await installDependencies({ @@ -418,12 +484,28 @@ export async function downloadRepo({ }); } - // 9) Done! relinka("success-verbose", "Repository downloaded successfully!"); - return { + const durationSeconds = (Date.now() - startTime) / 1000; + const result: DownloadResult = { source: repoURL, dir: projectPath, }; + if (returnTime) { + result.time = durationSeconds; + } + if (returnSize) { + // Convert size from bytes to megabytes (MB) and round to 2 decimal places. + // When preserveGit is false, exclude any ".git" folders. + const folderSizeBytes = await getFolderSize( + projectPath, + preserveGit ? [] : [".git"], + ); + result.size = parseFloat((folderSizeBytes / (1024 * 1024)).toFixed(2)); + } + if (returnConcurrency) { + result.concurrency = maxConcurrentProcesses; + } + return result; } catch (error) { relinka("error", "Failed to download repository..."); if (error instanceof Error) { diff --git a/src/utils/downloading/handleDownload.ts b/src/utils/downloading/handleDownload.ts index 9410d2b..6e463e4 100644 --- a/src/utils/downloading/handleDownload.ts +++ b/src/utils/downloading/handleDownload.ts @@ -43,8 +43,8 @@ export async function handleDownload({ projectName, selectedRepo, auth, - preserveGit = true, config, + preserveGit = true, install = false, isCustom = false, }: { @@ -157,7 +157,11 @@ export async function handleDownload({ // ------------------------------------------------- if (!projectPath) { try { - relinka("info", `Now I'm downloading the '${selectedRepo}' repo...`); + relinka( + "info", + `Now I'm downloading the '${selectedRepo}' repo...`, + "The download speed depends on your internet connection and GitHub limits.", + ); result = await downloadRepo({ repoURL: selectedRepo, projectName, @@ -167,8 +171,20 @@ export async function handleDownload({ preserveGit, ...(config ? { config } : {}), install, + returnTime: true, + returnSize: true, }); projectPath = result.dir; + if (result.time) { + const includesGit = preserveGit + ? " (size includes the .git folder)." + : "."; + relinka( + "success", + `Successfully downloaded repo to ${projectPath}`, + `It took ${result.time} seconds to download ${result.size} MB${includesGit}`, + ); + } } catch (error) { relinka("error", "Failed to download repo:", String(error)); throw error; diff --git a/src/utils/gdp-mod.ts b/src/utils/gdp-mod.ts index e96c06c..d452fa4 100644 --- a/src/utils/gdp-mod.ts +++ b/src/utils/gdp-mod.ts @@ -22,7 +22,7 @@ import { createVercelDeployment } from "~/app/menu/create-project/cp-modules/git import { getVercelProjectDomain } from "~/app/menu/create-project/cp-modules/git-deploy-prompts/vercel/vercel-domain.js"; import { isProjectDeployed } from "~/app/menu/create-project/cp-modules/git-deploy-prompts/vercel/vercel-mod.js"; import { decide } from "~/utils/decideHelper.js"; -import { handleReliverseMemory } from "~/utils/reliverseMemory.js"; +import { getReliverseMemory } from "~/utils/reliverseMemory.js"; /** * Collects details from a GitHub setup attempt. @@ -107,7 +107,7 @@ export async function configureGithubRepo( } // Read memory again to get the new GitHub token - const updatedMemory = await handleReliverseMemory(); + const updatedMemory = await getReliverseMemory(); if (!updatedMemory?.githubKey) { relinka("error", "GitHub token still not found after setup"); diff --git a/src/utils/handlers/generateProjectConfigs.ts b/src/utils/handlers/generateProjectConfigs.ts index 6b930ac..9a74ce7 100644 --- a/src/utils/handlers/generateProjectConfigs.ts +++ b/src/utils/handlers/generateProjectConfigs.ts @@ -157,6 +157,7 @@ export async function generateConfigFiles( projectRepository: `https://github.com/${effectiveAuthor}/${projectName}`, projectState: "creating", projectDomain: cleanDomain, + projectGitService: "github", projectDeployService: deployService, features: { ...DEFAULT_CONFIG.features, diff --git a/src/utils/handlers/handleCodemods.ts b/src/utils/handlers/handleCodemods.ts index fe696e5..5bdac2f 100644 --- a/src/utils/handlers/handleCodemods.ts +++ b/src/utils/handlers/handleCodemods.ts @@ -78,7 +78,7 @@ export async function handleCodemods(rules: ReliverseConfig, cwd: string) { } // Push: runtime conversion codemods - if (rules.projectRuntime === "nodejs") { + if (rules.projectRuntime === "node") { availableCodemods.push( { label: "Convert to Bun", diff --git a/src/utils/reliverseConfig.ts b/src/utils/reliverseConfig.ts index c3f2cf2..83bce39 100644 --- a/src/utils/reliverseConfig.ts +++ b/src/utils/reliverseConfig.ts @@ -2,11 +2,11 @@ import type { TSchema } from "@sinclair/typebox"; import type { PackageJson } from "pkg-types"; import { relinka } from "@reliverse/prompts"; +import { getUserPkgManager, isBunPM, runtimeInfo } from "@reliverse/runtime"; import { Type } from "@sinclair/typebox"; import { Value } from "@sinclair/typebox/value"; import { parseJSONC } from "confbox"; import destr, { safeDestr } from "destr"; -import { detect } from "detect-package-manager"; import fs from "fs-extra"; import path from "pathe"; import { readPackageJSON } from "pkg-types"; @@ -30,6 +30,14 @@ import { } from "./schemaConfig.js"; import { getCurrentWorkingDirectory } from "./terminalHelpers.js"; +/* ------------------------------------------------------------------ + * Constants & Utility Extensions + * ------------------------------------------------------------------ */ + +// Extensions for backup and temp files +const BACKUP_EXTENSION = ".backup"; +const TEMP_EXTENSION = ".tmp"; + /* ------------------------------------------------------------------ * TypeScript Types * ------------------------------------------------------------------ */ @@ -63,9 +71,13 @@ export type ProjectFeatures = { }; /* ------------------------------------------------------------------ - * Re-reading the .reliverse file + * Re-reading the .reliverse File * ------------------------------------------------------------------ */ +/** + * Attempts to read the .reliverse config from the current working directory. + * If the file is not valid, it attempts a line-by-line fix. + */ export async function reReadReliverseConfig(): Promise { const cwd = getCurrentWorkingDirectory(); const configPath = path.join(cwd, ".reliverse"); @@ -73,7 +85,7 @@ export async function reReadReliverseConfig(): Promise { // First try normal read let config = await readReliverseConfig(configPath); - // If not valid, attempt line-by-line fix + // If not valid, attempt a line-by-line fix if (!config) { config = await parseAndFixConfig(configPath); } @@ -87,7 +99,7 @@ export async function reReadReliverseConfig(): Promise { export const PROJECT_FRAMEWORK_FILES: Record = { unknown: [], - "npm-jsr": ["jsr.json", "jsr.jsonc"], + "npm-jsr": ["jsr.json", "jsr.jsonc", "pub.config.ts"], astro: ["astro.config.js", "astro.config.ts", "astro.config.mjs"], nextjs: ["next.config.js", "next.config.ts", "next.config.mjs"], vite: ["vite.config.js", "vite.config.ts", "react.config.js"], @@ -111,11 +123,11 @@ export async function detectProjectFramework( } /* ------------------------------------------------------------------ - * Update project config + * Update Project Config * ------------------------------------------------------------------ */ /** - * Deep merges objects while preserving types and nested structures + * Deep merges two objects recursively while preserving nested structures. */ function deepMerge>( target: T, @@ -135,12 +147,9 @@ function deepMerge>( typeof targetValue === "object" && !Array.isArray(targetValue) ) { - result[key] = deepMerge(targetValue, sourceValue as any) as T[Extract< - keyof T, - string - >]; + result[key] = deepMerge(targetValue, sourceValue as any); } else { - result[key] = sourceValue as T[Extract]; + result[key] = sourceValue; } } } @@ -148,7 +157,8 @@ function deepMerge>( } /** - * Updates project configuration files with new values while preserving existing data + * Updates project configuration by merging new updates with the existing config. + * Creates a backup before overwriting and attempts to restore from backup on error. */ export async function updateReliverseConfig( projectPath: string, @@ -173,7 +183,7 @@ export async function updateReliverseConfig( } } - // Merge updates with existing config + // Merge updates with the existing config const mergedConfig = deepMerge(existingConfig, updates); // Validate final config @@ -189,17 +199,17 @@ export async function updateReliverseConfig( return false; } - // Create backup if file exists + // Create a backup if the file exists if (await fs.pathExists(configPath)) { await fs.copy(configPath, backupPath); } - // Write to temp file first + // Write to a temporary file first (atomic write) let fileContent = JSON.stringify(mergedConfig, null, 2); fileContent = injectSectionComments(fileContent); await fs.writeFile(tempPath, fileContent); - // Rename temp to actual file + // Rename temp file to actual config file await fs.rename(tempPath, configPath); // Remove backup on success @@ -210,23 +220,35 @@ export async function updateReliverseConfig( relinka("success", "Reliverse config updated successfully"); return true; } catch (error) { - // Restore from backup if write failed + relinka("error", "Failed to update .reliverse config:", String(error)); + // Attempt to restore from backup if write failed if ( (await fs.pathExists(backupPath)) && !(await fs.pathExists(configPath)) ) { - await fs.copy(backupPath, configPath); - relinka("warn", "Restored config from backup after failed update"); + try { + await fs.copy(backupPath, configPath); + relinka("warn", "Restored config from backup after failed update"); + } catch (restoreError) { + relinka( + "error", + "Failed to restore config from backup:", + String(restoreError), + ); + } } - // Clean up temp file + // Clean up temporary file if it exists if (await fs.pathExists(tempPath)) { await fs.remove(tempPath); } - relinka("error", "Failed to update .reliverse config:", String(error)); return false; } } +/** + * Migrates an external .reliverse file into the current project config. + * Only migrates fields that exist in the current schema. + */ export async function migrateReliverseConfig( externalReliversePath: string, projectPath: string, @@ -241,121 +263,56 @@ export async function migrateReliverseConfig( const tempConfig = parsed as Partial; const migratedFields: string[] = []; - - // Only migrate fields that match our schema - const validConfig: Partial = { - // Project info - ...(tempConfig.projectDescription && { - projectDescription: tempConfig.projectDescription, - [migratedFields.push("projectDescription")]: undefined, - }), - ...(tempConfig.projectVersion && { - projectVersion: tempConfig.projectVersion, - [migratedFields.push("projectVersion")]: undefined, - }), - ...(tempConfig.projectLicense && { - projectLicense: tempConfig.projectLicense, - [migratedFields.push("projectLicense")]: undefined, - }), - ...(tempConfig.projectRepository && { - projectRepository: tempConfig.projectRepository, - [migratedFields.push("projectRepository")]: undefined, - }), - ...(tempConfig.projectCategory && { - projectCategory: tempConfig.projectCategory, - [migratedFields.push("projectCategory")]: undefined, - }), - ...(tempConfig.projectSubcategory && { - projectSubcategory: tempConfig.projectSubcategory, - [migratedFields.push("projectSubcategory")]: undefined, - }), - ...(tempConfig.projectFramework && { - projectFramework: tempConfig.projectFramework, - [migratedFields.push("projectFramework")]: undefined, - }), - ...(tempConfig.projectTemplate && { - projectTemplate: tempConfig.projectTemplate, - [migratedFields.push("projectTemplate")]: undefined, - }), - ...(tempConfig.projectArchitecture && { - projectArchitecture: tempConfig.projectArchitecture, - [migratedFields.push("projectArchitecture")]: undefined, - }), - ...(tempConfig.projectRuntime && { - projectRuntime: tempConfig.projectRuntime, - [migratedFields.push("projectRuntime")]: undefined, - }), - - // Features and preferences - ...(tempConfig.features && { - features: tempConfig.features, - [migratedFields.push("features")]: undefined, - }), - ...(tempConfig.preferredLibraries && { - preferredLibraries: tempConfig.preferredLibraries, - [migratedFields.push("preferredLibraries")]: undefined, - }), - ...(tempConfig.codeStyle && { - codeStyle: tempConfig.codeStyle, - [migratedFields.push("codeStyle")]: undefined, - }), - ...(tempConfig.monorepo && { - monorepo: tempConfig.monorepo, - [migratedFields.push("monorepo")]: undefined, - }), - ...(tempConfig.ignoreDependencies && { - ignoreDependencies: tempConfig.ignoreDependencies, - [migratedFields.push("ignoreDependencies")]: undefined, - }), - ...(tempConfig.customRules && { - customRules: tempConfig.customRules, - [migratedFields.push("customRules")]: undefined, - }), - - // Behaviors - ...(tempConfig.skipPromptsUseAutoBehavior !== undefined && { - skipPromptsUseAutoBehavior: tempConfig.skipPromptsUseAutoBehavior, - [migratedFields.push("skipPromptsUseAutoBehavior")]: undefined, - }), - ...(tempConfig.deployBehavior && { - deployBehavior: tempConfig.deployBehavior, - [migratedFields.push("deployBehavior")]: undefined, - }), - ...(tempConfig.depsBehavior && { - depsBehavior: tempConfig.depsBehavior, - [migratedFields.push("depsBehavior")]: undefined, - }), - ...(tempConfig.gitBehavior && { - gitBehavior: tempConfig.gitBehavior, - [migratedFields.push("gitBehavior")]: undefined, - }), - ...(tempConfig.i18nBehavior && { - i18nBehavior: tempConfig.i18nBehavior, - [migratedFields.push("i18nBehavior")]: undefined, - }), - ...(tempConfig.scriptsBehavior && { - scriptsBehavior: tempConfig.scriptsBehavior, - [migratedFields.push("scriptsBehavior")]: undefined, - }), - ...(tempConfig.existingRepoBehavior && { - existingRepoBehavior: tempConfig.existingRepoBehavior, - [migratedFields.push("existingRepoBehavior")]: undefined, - }), - ...(tempConfig.repoPrivacy && { - repoPrivacy: tempConfig.repoPrivacy, - [migratedFields.push("repoPrivacy")]: undefined, - }), - }; + // Declare validConfig as a partial record keyed by ReliverseConfig keys. + const validConfig = {} as Partial>; + + // List of keys to consider for migration + const keysToMigrate: (keyof ReliverseConfig)[] = [ + "projectDescription", + "projectVersion", + "projectLicense", + "projectRepository", + "projectCategory", + "projectSubcategory", + "projectFramework", + "projectTemplate", + "projectArchitecture", + "deployBehavior", + "depsBehavior", + "gitBehavior", + "i18nBehavior", + "scriptsBehavior", + "existingRepoBehavior", + "repoPrivacy", + "features", + "preferredLibraries", + "codeStyle", + "monorepo", + "ignoreDependencies", + "customRules", + "skipPromptsUseAutoBehavior", + ]; + + for (const key of keysToMigrate) { + if (tempConfig[key] !== undefined) { + // Using the type assertion to assign the value into our validConfig. + validConfig[key] = tempConfig[key]; + migratedFields.push(String(key)); + } + } // Update the .reliverse config with migrated data - const success = await updateReliverseConfig(projectPath, validConfig); + const success = await updateReliverseConfig( + projectPath, + validConfig as Partial, + ); if (success) { relinka("success", "Successfully migrated .reliverse config"); relinka("success-verbose", "Migrated fields:", migratedFields.join(", ")); } - // Clean up temp.reliverse after migration + // Clean up external file after migration await fs.remove(externalReliversePath); } catch (error) { relinka( @@ -367,34 +324,30 @@ export async function migrateReliverseConfig( } /* ------------------------------------------------------------------ - * Default + Merging Logic + * Default Config and Merging Logic * ------------------------------------------------------------------ */ export const DEFAULT_CONFIG: ReliverseConfig = { - // Reliverse config schema $schema: RELIVERSE_SCHEMA_URL, - - // General project information projectName: UNKNOWN_VALUE, projectAuthor: UNKNOWN_VALUE, projectDescription: UNKNOWN_VALUE, projectVersion: "0.1.0", projectLicense: "MIT", - projectRepository: UNKNOWN_VALUE, projectState: "creating", + projectRepository: DEFAULT_DOMAIN, projectDomain: DEFAULT_DOMAIN, - projectDeployService: "vercel", projectCategory: UNKNOWN_VALUE, projectSubcategory: UNKNOWN_VALUE, projectTemplate: UNKNOWN_VALUE, projectArchitecture: UNKNOWN_VALUE, repoPrivacy: UNKNOWN_VALUE, + projectGitService: "github", + projectDeployService: "vercel", repoBranch: "main", - - // Primary tech stack/framework projectFramework: "nextjs", - projectPackageManager: "bun", - projectRuntime: "nodejs", + projectPackageManager: (await isBunPM()) ? "bun" : "npm", + projectRuntime: runtimeInfo?.name || "node", preferredLibraries: { stateManagement: "zustand", formManagement: "react-hook-form", @@ -453,14 +406,11 @@ export const DEFAULT_CONFIG: ReliverseConfig = { replaceEvents: false, }, }, - - // Custom repos configuration multipleRepoCloneMode: false, customUserFocusedRepos: [], customDevsFocusedRepos: [], hideRepoSuggestions: false, customReposOnNewProject: false, - envComposerOpenBrowser: true, skipPromptsUseAutoBehavior: false, deployBehavior: "prompt", @@ -471,9 +421,6 @@ export const DEFAULT_CONFIG: ReliverseConfig = { existingRepoBehavior: "prompt", }; -/** - * Merges a partial config with defaults, preserving nested objects. - */ function mergeWithDefaults(partial: Partial): ReliverseConfig { return { ...DEFAULT_CONFIG, @@ -508,9 +455,12 @@ function mergeWithDefaults(partial: Partial): ReliverseConfig { } /* ------------------------------------------------------------------ - * fixLineByLine + * Fixing Config Line-by-Line * ------------------------------------------------------------------ */ +/** + * Creates a schema for a single property so that it can be validated in isolation. + */ function createSinglePropertySchema(key: string, subSchema: TSchema): TSchema { return Type.Object({ [key]: subSchema } as Record, { additionalProperties: false, @@ -518,6 +468,9 @@ function createSinglePropertySchema(key: string, subSchema: TSchema): TSchema { }); } +/** + * Validates a single property against its schema. + */ function fixSingleProperty( schema: TSchema, propName: string, @@ -532,17 +485,14 @@ function fixSingleProperty( } /** - * Recursively fix each property in an object, falling back to defaults if invalid. - * Return an array of property paths that were changed. + * Recursively fixes each property in the object. Returns the fixed config and + * an array of property paths that were changed. */ export function fixLineByLine( userConfig: unknown, defaultConfig: unknown, schema: TSchema, -): { - fixedConfig: unknown; - changedKeys: string[]; -} { +): { fixedConfig: unknown; changedKeys: string[] } { const isObjectSchema = (schema as any).type === "object" && (schema as any).properties; @@ -568,14 +518,13 @@ export function fixLineByLine( const userValue = (userConfig as any)[propName]; const defaultValue = (defaultConfig as any)[propName]; - // Track missing fields and inject defaults if (userValue === undefined && !(propName in userConfig)) { missingKeys.push(propName); result[propName] = defaultValue; continue; } - // Special handling for custom repository arrays + // Special handling for repository arrays if ( propName === "customUserFocusedRepos" || propName === "customDevsFocusedRepos" @@ -647,28 +596,29 @@ export function injectSectionComments(fileContent: string): string { ], projectName: [comment("General project information")], skipPromptsUseAutoBehavior: [ - comment("Do you want to enable auto-answering for prompts?"), - comment("Set this field to true to skip manual confirmations."), - comment("Configure also unknown values and prompts behavior."), + comment( + "Enable auto-answering for prompts to skip manual confirmations.", + ), + comment("Make sure you have unknown values configured above."), ], features: [comment("Project features")], projectFramework: [comment("Primary tech stack/framework")], codeStyle: [comment("Code style preferences")], - multipleRepoCloneMode: [comment("`Clone an existing repo` menu")], + multipleRepoCloneMode: [comment("Settings for cloning an existing repo")], envComposerOpenBrowser: [ - comment("Set to false to disable opening"), - comment("the browser while env composing"), + comment( + "Set to false to disable opening the browser during env composing", + ), ], - ignoreDependencies: [comment("Dependencies to exclude from checks")], + ignoreDependencies: [comment("List dependencies to exclude from checks")], customRules: [comment("Custom rules for Reliverse AI")], deployBehavior: [ - comment("Specific prompts behavior"), - comment("prompt | autoYes | autoNo"), + comment("Prompt behavior for deployment"), + comment("Options: prompt | autoYes | autoNo"), ], existingRepoBehavior: [ - comment("What CLI should do with existing GitHub repo"), - comment("Applicable for the new project creation only"), - comment("prompt | autoYes | autoYesSkipCommit | autoNo"), + comment("Behavior for existing GitHub repos during project creation"), + comment("Options: prompt | autoYes | autoYesSkipCommit | autoNo"), ], }; @@ -693,30 +643,13 @@ export function injectSectionComments(fileContent: string): string { } /* ------------------------------------------------------------------ - * Constants + * Config Read/Write (TypeBox) * ------------------------------------------------------------------ */ -const BACKUP_EXTENSION = ".backup"; -const TEMP_EXTENSION = ".tmp"; /** - * Cleans GitHub repository URLs by removing git+ prefix and .git suffix + * Parses the .reliverse file and validates it against the schema. + * Returns both the parsed object and any errors (if present). */ -export function cleanGitHubUrl(url: string): string { - return url - .trim() - .replace(/^git\+/, "") - .replace( - /^https?:\/\/(www\.)?(github|gitlab|bitbucket|sourcehut)\.com\//i, - "", - ) - .replace(/^(github|gitlab|bitbucket|sourcehut)\.com\//i, "") - .replace(/\.git$/i, ""); -} - -/* ------------------------------------------------------------------ - * Config Read/Write (TypeBox) - * ------------------------------------------------------------------ */ - async function parseReliverseFile(configPath: string): Promise<{ parsed: unknown; errors: Iterable<{ @@ -751,6 +684,10 @@ async function parseReliverseFile(configPath: string): Promise<{ } } +/** + * Writes the given ReliverseConfig to the config file. + * Uses an atomic write (via a temporary file) and creates a backup. + */ export async function writeReliverseConfig( configPath: string, config: ReliverseConfig, @@ -798,6 +735,10 @@ export async function writeReliverseConfig( } } +/** + * Reads and validates the .reliverse config file. + * If errors are detected, it attempts to merge missing or invalid fields with defaults. + */ export async function readReliverseConfig( configPath: string, ): Promise { @@ -850,6 +791,10 @@ export async function readReliverseConfig( * parseAndFixConfig (Line-by-Line) * ------------------------------------------------------------------ */ +/** + * Reads the .reliverse file, fixes invalid lines based on the schema, + * writes back the fixed config, and returns the fixed config. + */ async function parseAndFixConfig( configPath: string, ): Promise { @@ -872,18 +817,15 @@ async function parseAndFixConfig( if (Value.Check(reliverseConfigSchema, fixedConfig)) { await writeReliverseConfig(configPath, fixedConfig); const originalInvalidPaths = originalErrors.map((err) => err.path); - relinka( "info", "Your .reliverse config has been fixed. Please ensure it aligns with your project.", `Changed keys: ${changedKeys.join(", ") || "(none)"}`, ); - relinka( "info-verbose", - `Invalid paths were: ${originalInvalidPaths.join(", ") || "(none)"}; `, + `Originally invalid paths were: ${originalInvalidPaths.join(", ") || "(none)"}`, ); - return fixedConfig; } else { const newErrs = [ @@ -908,7 +850,7 @@ async function parseAndFixConfig( } /* ------------------------------------------------------------------ - * Generating a Default Config + * Generating a Default Config and Merging with Detected Data * ------------------------------------------------------------------ */ export async function getDefaultReliverseConfig( @@ -931,7 +873,7 @@ export async function getDefaultReliverseConfig( } const biomeConfig = await getBiomeConfig(cwd); - const detectedPkgManager = await detect(); + const detectedPkgManager = await getUserPkgManager(cwd); const packageJsonPath = path.join(cwd, "package.json"); let packageData: PackageJson = { @@ -943,7 +885,7 @@ export async function getDefaultReliverseConfig( try { packageData = await readPackageJSON(cwd); } catch { - // fallback + // fallback if reading fails } } @@ -959,10 +901,11 @@ export async function getDefaultReliverseConfig( projectRepository: typeof packageData.repository === "string" ? packageData.repository - : (packageData.repository?.url ?? UNKNOWN_VALUE), + : (packageData.repository?.url ?? DEFAULT_DOMAIN), projectState: "creating", projectDomain: effectiveProjectName === cliName ? cliDomain : DEFAULT_DOMAIN, + projectGitService: "github", projectDeployService: "vercel", projectCategory: UNKNOWN_VALUE, projectSubcategory: UNKNOWN_VALUE, @@ -970,11 +913,9 @@ export async function getDefaultReliverseConfig( projectArchitecture: UNKNOWN_VALUE, repoPrivacy: UNKNOWN_VALUE, repoBranch: "main", - - // Primary tech stack/framework projectFramework: detectedProjectFramework ?? UNKNOWN_VALUE, - projectPackageManager: detectedPkgManager, - projectRuntime: "nodejs", + projectPackageManager: detectedPkgManager.packageManager, + projectRuntime: runtimeInfo?.name || "node", codeStyle: { ...DEFAULT_CONFIG.codeStyle, lineWidth: biomeConfig?.lineWidth ?? 80, @@ -985,43 +926,140 @@ export async function getDefaultReliverseConfig( } /* ------------------------------------------------------------------ - * Safely reading package.json + * Project Detection and Additional Logic * ------------------------------------------------------------------ */ -async function getPackageJson( - projectPath: string, -): Promise { - try { - const packageJsonPath = path.join(projectPath, "package.json"); - if (!(await fs.pathExists(packageJsonPath))) { - return null; - } - return await readPackageJSON(projectPath); - } catch (error) { - const packageJsonPath = path.join(projectPath, "package.json"); - if (await fs.pathExists(packageJsonPath)) { - relinka( - "warn", - "Could not read package.json:", - error instanceof Error ? error.message : String(error), - ); - } - return null; + +async function checkProjectFiles(projectPath: string): Promise<{ + hasReliverse: boolean; + hasPackageJson: boolean; + hasNodeModules: boolean; + hasGit: boolean; +}> { + const [hasReliverse, hasPackageJson, hasNodeModules, hasGit] = + await Promise.all([ + fs.pathExists(path.join(projectPath, ".reliverse")), + fs.pathExists(path.join(projectPath, "package.json")), + fs.pathExists(path.join(projectPath, "node_modules")), + fs.pathExists(path.join(projectPath, ".git")), + ]); + + return { hasReliverse, hasPackageJson, hasNodeModules, hasGit }; +} + +export type DetectedProject = { + name: string; + path: string; + config: ReliverseConfig; + gitStatus?: { + uncommittedChanges: number; + unpushedCommits: number; + }; + needsDepsInstall?: boolean; + hasGit?: boolean; +}; + +/* ------------------------------------------------------------------ + * Reliverse Config Creation (wrapper around config generator and fixer) + * ------------------------------------------------------------------ */ + +async function createReliverseConfig( + cwd: string, + githubUsername: string, + isDev: boolean, +): Promise { + const defaultRules = await generateDefaultRulesForProject(cwd); + + const effectiveProjectName = defaultRules?.projectName ?? path.basename(cwd); + let effectiveAuthorName = defaultRules?.projectAuthor ?? UNKNOWN_VALUE; + const effectiveDomain = + defaultRules?.projectDomain ?? + (effectiveProjectName === cliName ? cliDomain : DEFAULT_DOMAIN); + + if (isDev) { + effectiveAuthorName = + effectiveAuthorName === "reliverse" ? "blefnk" : effectiveAuthorName; } + + await generateReliverseConfig({ + projectName: effectiveProjectName, + cliUsername: effectiveAuthorName, + deployService: "vercel", + primaryDomain: effectiveDomain, + projectPath: cwd, + githubUsername, + isDev, + }); + + relinka( + "info-verbose", + defaultRules + ? "Created .reliverse configuration based on detected project settings." + : "Created initial .reliverse configuration. Please review and adjust as needed.", + ); } /* ------------------------------------------------------------------ - * Project Detection & Additional Logic + * Multi-config Reading from `reli` Folder * ------------------------------------------------------------------ */ -export async function getPackageJsonSafe( + +/** + * Reads all `*.reliverse` files in the `reli` folder, parses and fixes them if needed. + * Returns an array of valid ReliverseConfigs. + */ +export async function readReliverseConfigsInReliFolder( cwd: string, -): Promise { - const packageJsonPath = path.join(cwd, "package.json"); - if (!(await fs.pathExists(packageJsonPath))) { - return null; +): Promise { + const reliFolderPath = path.join(cwd, "reli"); + const results: ReliverseConfig[] = []; + + if (!(await fs.pathExists(reliFolderPath))) { + return results; } - return await readPackageJSON(cwd); + + const dirItems = await fs.readdir(reliFolderPath); + const reliverseFiles = dirItems.filter((item) => item.endsWith(".reliverse")); + + // Process each file concurrently + const configs = await Promise.all( + reliverseFiles.map(async (file) => { + const filePath = path.join(reliFolderPath, file); + let config = await readReliverseConfig(filePath); + if (!config) { + config = await parseAndFixConfig(filePath); + } + if (!config) { + relinka("warn", `Skipping invalid config file: ${filePath}`); + } + return config; + }), + ); + + return configs.filter((cfg): cfg is ReliverseConfig => cfg !== null); } +/* ------------------------------------------------------------------ + * Clean GitHub URL + * ------------------------------------------------------------------ */ + +/** + * Cleans GitHub repository URLs by removing git+ prefix and .git suffix. + */ +export function cleanGitHubUrl(url: string): string { + return url + .trim() + .replace(/^git\+/, "") + .replace( + /^https?:\/\/(www\.)?(github|gitlab|bitbucket|sourcehut)\.com\//i, + "", + ) + .replace(/^(github|gitlab|bitbucket|sourcehut)\.com\//i, "") + .replace(/\.git$/i, ""); +} + +/* ------------------------------------------------------------------ + * Generating Default Rules for Project + * ------------------------------------------------------------------ */ + export async function generateDefaultRulesForProject( cwd: string, ): Promise { @@ -1093,33 +1131,40 @@ export async function generateDefaultRulesForProject( return rules; } -export type DetectedProject = { - name: string; - path: string; - config: ReliverseConfig; - gitStatus?: { - uncommittedChanges: number; - unpushedCommits: number; - }; - needsDepsInstall?: boolean; - hasGit?: boolean; -}; +/* ------------------------------------------------------------------ + * Project Detection & Additional Logic + * ------------------------------------------------------------------ */ -async function checkProjectFiles(projectPath: string): Promise<{ - hasReliverse: boolean; - hasPackageJson: boolean; - hasNodeModules: boolean; - hasGit: boolean; -}> { - const [hasReliverse, hasPackageJson, hasNodeModules, hasGit] = - await Promise.all([ - fs.pathExists(path.join(projectPath, ".reliverse")), - fs.pathExists(path.join(projectPath, "package.json")), - fs.pathExists(path.join(projectPath, "node_modules")), - fs.pathExists(path.join(projectPath, ".git")), - ]); +async function getPackageJson( + projectPath: string, +): Promise { + try { + const packageJsonPath = path.join(projectPath, "package.json"); + if (!(await fs.pathExists(packageJsonPath))) { + return null; + } + return await readPackageJSON(projectPath); + } catch (error) { + const packageJsonPath = path.join(projectPath, "package.json"); + if (await fs.pathExists(packageJsonPath)) { + relinka( + "warn", + "Could not read package.json:", + error instanceof Error ? error.message : String(error), + ); + } + return null; + } +} - return { hasReliverse, hasPackageJson, hasNodeModules, hasGit }; +export async function getPackageJsonSafe( + cwd: string, +): Promise { + const packageJsonPath = path.join(cwd, "package.json"); + if (!(await fs.pathExists(packageJsonPath))) { + return null; + } + return await readPackageJSON(cwd); } export async function detectProject( @@ -1168,13 +1213,18 @@ export async function detectProjectsWithReliverse( try { const items = await fs.readdir(cwd, { withFileTypes: true }); - for (const item of items) { - if (item.isDirectory()) { - const projectPath = path.join(cwd, item.name); - const project = await detectProject(projectPath); - if (project) { - detected.push(project); - } + // Process directories concurrently + const subProjects = await Promise.all( + items + .filter((item) => item.isDirectory()) + .map(async (item) => { + const projectPath = path.join(cwd, item.name); + return await detectProject(projectPath); + }), + ); + for (const project of subProjects) { + if (project) { + detected.push(project); } } } catch (error) { @@ -1265,14 +1315,18 @@ export async function generateReliverseConfig({ packageJson?.description ?? baseRules.projectDescription ?? UNKNOWN_VALUE; baseRules.projectVersion = packageJson?.version ?? baseRules.projectVersion; baseRules.projectLicense = packageJson?.license ?? baseRules.projectLicense; + + const projectNameWithoutAt = projectName?.replace("@", ""); + baseRules.projectRepository = packageJson?.repository ? typeof packageJson.repository === "string" ? cleanGitHubUrl(packageJson.repository) : cleanGitHubUrl(packageJson.repository.url) : githubUsername && projectName - ? `${githubUsername}/${projectName}` - : UNKNOWN_VALUE; + ? `https://github.com/${projectNameWithoutAt}` + : DEFAULT_DOMAIN; + baseRules.projectGitService = "github"; baseRules.projectDeployService = deployService; baseRules.projectDomain = primaryDomain ? `https://${primaryDomain.replace(/^https?:\/\//, "")}` @@ -1334,7 +1388,7 @@ export async function generateReliverseConfig({ const content = await fs.readFile(configPath, "utf-8"); existingContent = destr(content); } catch { - // ignore + // ignore errors } } @@ -1352,104 +1406,23 @@ export async function generateReliverseConfig({ await writeReliverseConfig(configPath, effectiveConfig); } -async function createReliverseConfig( - cwd: string, - githubUsername: string, - isDev: boolean, -): Promise { - const defaultRules = await generateDefaultRulesForProject(cwd); - - const effectiveProjectName = defaultRules?.projectName ?? path.basename(cwd); - let effectiveAuthorName = defaultRules?.projectAuthor ?? UNKNOWN_VALUE; - const effectiveDomain = - defaultRules?.projectDomain ?? - (effectiveProjectName === cliName ? cliDomain : DEFAULT_DOMAIN); - - if (isDev) { - effectiveAuthorName = - effectiveAuthorName === "reliverse" ? "blefnk" : effectiveAuthorName; - } - - await generateReliverseConfig({ - projectName: effectiveProjectName, - cliUsername: effectiveAuthorName, - deployService: "vercel", - primaryDomain: effectiveDomain, - projectPath: cwd, - githubUsername, - isDev, - }); - - relinka( - "info-verbose", - defaultRules - ? "Created .reliverse configuration based on detected project settings." - : "Created initial .reliverse configuration. Please review and adjust as needed.", - ); -} - -/* ------------------------------------------------------------------ - * Multi-config reading from `reli` folder - * ------------------------------------------------------------------ */ - -/** - * Reads all `*.reliverse` files in the `reli` folder, parses them, and - * fixes them if needed. Returns an array of valid ReliverseConfigs. - */ -export async function readReliverseConfigsInReliFolder( - cwd: string, -): Promise { - const reliFolderPath = path.join(cwd, "reli"); - const results: ReliverseConfig[] = []; - - // If the folder doesn't exist, return empty array - if (!(await fs.pathExists(reliFolderPath))) { - return results; - } - - const dirItems = await fs.readdir(reliFolderPath); - const reliverseFiles = dirItems.filter((item) => item.endsWith(".reliverse")); - - for (const file of reliverseFiles) { - const filePath = path.join(reliFolderPath, file); - let config = await readReliverseConfig(filePath); - - // If not valid, attempt line-by-line fix - if (!config) { - config = await parseAndFixConfig(filePath); - } - - // If still not valid, skip - if (!config) { - relinka("warn", `Skipping invalid config file: ${filePath}`); - continue; - } - - results.push(config); - } - - return results; -} - /* ------------------------------------------------------------------ * The Core Logic: Handle or Verify `.reliverse` + MULTI-CONFIG * ------------------------------------------------------------------ */ -export async function handleReliverseConfig( +export async function getReliverseConfig( cwd: string, isDev: boolean, ): Promise<{ config: ReliverseConfig; reli: ReliverseConfig[] }> { const githubUsername = UNKNOWN_VALUE; - // 1. First, detect multi-config folder "reli" + // 1. Detect multi-config folder "reli" const reliFolderPath = path.join(cwd, "reli"); const hasReliFolder = await fs.pathExists(reliFolderPath); let reliConfigs: ReliverseConfig[] = []; - // 2. If `reli` folder exists, read all `*.reliverse` files in it if (hasReliFolder) { reliConfigs = await readReliverseConfigsInReliFolder(cwd); - if (reliConfigs.length > 0) { relinka( "info-verbose", @@ -1458,9 +1431,8 @@ export async function handleReliverseConfig( } } - // 3. Handle single `.reliverse` in root + // 2. Handle single .reliverse file in the root const configPath = path.join(cwd, ".reliverse"); - if (!(await fs.pathExists(configPath))) { await createReliverseConfig(cwd, githubUsername, isDev); } else { @@ -1490,16 +1462,9 @@ export async function handleReliverseConfig( return { config: { ...DEFAULT_CONFIG }, reli: reliConfigs }; } - // Update schema URL if in dev mode if (isDev) { mainConfig.$schema = RELIVERSE_SCHEMA_DEV; } return { config: mainConfig, reli: reliConfigs }; } - -export async function getReliverseConfig() { - const cwd = getCurrentWorkingDirectory(); - const { config } = await handleReliverseConfig(cwd, true); - return config; -} diff --git a/src/utils/reliverseMemory.ts b/src/utils/reliverseMemory.ts index 695aa84..e85a2ca 100644 --- a/src/utils/reliverseMemory.ts +++ b/src/utils/reliverseMemory.ts @@ -93,7 +93,7 @@ export async function reReadReliverseMemory(): Promise { } } -export async function handleReliverseMemory(): Promise { +export async function getReliverseMemory(): Promise { // Ensure directory exists const homeDir = os.homedir(); const memoryFile = path.join(homeDir, MEMORY_FILE); diff --git a/src/utils/schemaConfig.ts b/src/utils/schemaConfig.ts index af6ac8c..67b43d8 100644 --- a/src/utils/schemaConfig.ts +++ b/src/utils/schemaConfig.ts @@ -100,6 +100,12 @@ export const reliverseConfigSchema = Type.Object({ projectLicense: Type.String(), projectRepository: Type.String(), projectDomain: Type.String(), + projectGitService: Type.Union([ + Type.Literal("github"), + Type.Literal("gitlab"), + Type.Literal("bitbucket"), + Type.Literal("none"), + ]), projectDeployService: Type.Union([ Type.Literal("vercel"), Type.Literal("netlify"), @@ -187,9 +193,13 @@ export const reliverseConfigSchema = Type.Object({ Type.Literal("separated"), ]), projectRuntime: Type.Union([ - Type.Literal("nodejs"), - Type.Literal("deno"), Type.Literal("bun"), + Type.Literal("deno"), + Type.Literal("edge-light"), + Type.Literal("fastly"), + Type.Literal("netlify"), + Type.Literal("node"), + Type.Literal("workerd"), ]), skipPromptsUseAutoBehavior: Type.Boolean(),