diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..a6bdcfc --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,40 @@ +name: Generate Changelog + +permissions: + contents: write + +on: + push: + branches-ignore: + - main + +jobs: + changelog: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm install + + - name: Generate CHANGELOG.md + run: npm run changelog + + - name: Commit CHANGELOG.md + run: | + git config user.name "github-actions" + git config user.email "github-actions@github.com" + + git add CHANGELOG.md + git diff --cached --quiet || git commit -m "chore(changelog): update changelog" + + git push diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c1117d8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,72 @@ +name: Release + +on: + push: + branches: + - main + +jobs: + release: + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm install + + - name: Read version from package.json + id: version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Check if tag already exists + id: tag + run: | + if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Stop if release already exists + if: steps.tag.outputs.exists == 'true' + run: | + echo "Release v${{ steps.version.outputs.version }} already exists. Skipping." + exit 0 + + - name: Generate CHANGELOG + run: npm run changelog + + - name: Commit CHANGELOG + run: | + git config user.name "github-actions" + git config user.email "github-actions@github.com" + git add CHANGELOG.md + git commit -m "chore(changelog): release v${{ steps.version.outputs.version }}" || echo "No changes" + + - name: Push changelog commit + run: git push + + - name: Create tag + run: | + git tag v${{ steps.version.outputs.version }} + git push origin v${{ steps.version.outputs.version }} + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.version.outputs.version }} + generate_release_notes: true diff --git a/.github/workflows/validate-version-pr.yml b/.github/workflows/validate-version-pr.yml new file mode 100644 index 0000000..3926b4e --- /dev/null +++ b/.github/workflows/validate-version-pr.yml @@ -0,0 +1,41 @@ +name: Validate version on PR + +on: + pull_request: + branches: + - main + +jobs: + validate-version: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Validate version against source branch + run: | + SOURCE_BRANCH="${{ github.head_ref }}" + VERSION=$(node -p "require('./package.json').version") + + echo "Source branch: $SOURCE_BRANCH" + echo "Package version: $VERSION" + + if [[ "$SOURCE_BRANCH" =~ ^([0-9]+)\.([0-9]+)\.x$ ]]; then + EXPECTED_MAJOR_MINOR="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" + ACTUAL_MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2) + + if [[ "$EXPECTED_MAJOR_MINOR" != "$ACTUAL_MAJOR_MINOR" ]]; then + echo "❌ Version mismatch!" + echo "Branch expects version: $EXPECTED_MAJOR_MINOR.x" + echo "Found version: $VERSION" + exit 1 + fi + else + echo "ℹ️ Branch does not follow version pattern, skipping validation." + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ccb2c80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3634cc6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ + +# 1.0.0 (2025-12-22) + +### cli +| Commit | Type | Description | +|--------|------|-------------| +| [c6e6047](https://github.com/DIGOARTHUR/github-automated-repos-cli/commit/c6e60477c5eca20e009e4c052afa6b90bbf08e21) | feat | add prompts-based overwrite options and support for creating alternative project files | +| [52780d8](https://github.com/DIGOARTHUR/github-automated-repos-cli/commit/52780d83456d14506e87d6517203c085afa68a52) | feat | integrate ora loading spinner for setup steps | + + +### page +| Commit | Type | Description | +|--------|------|-------------| +| [0e934f9](https://github.com/DIGOARTHUR/github-automated-repos-cli/commit/0e934f945abafc55d5c2fd97996dd8a6e26cbeba) | feat | add structions cards: icons, topics, explain (WIP) | +| [104b7fd](https://github.com/DIGOARTHUR/github-automated-repos-cli/commit/104b7fd867a1590d6e1a49dfa3a5542c7012e4a6) | feat | implement page vitejs example | + + +### vite-template +| Commit | Type | Description | +|--------|------|-------------| +| [b4870e1](https://github.com/DIGOARTHUR/github-automated-repos-cli/commit/b4870e1cddadc9fbc23edd14bf848fb42f6454dc) | fix | replace static username and keyword with dynamic placeholders | + + +### general +| Commit | Type | Description | +|--------|------|-------------| +| [d30acd8](https://github.com/DIGOARTHUR/github-automated-repos-cli/commit/d30acd832144816168a914a5c36d61e11bb101c6) | feat | build example page | +| [2234519](https://github.com/DIGOARTHUR/github-automated-repos-cli/commit/223451939421eb7185d48f99fe5890e817697221) | feat | implement an interactive command to enter GitHub Username and Keyword | + + diff --git a/bin/index.js b/bin/index.js index 65158d7..c6e4c96 100755 --- a/bin/index.js +++ b/bin/index.js @@ -1,41 +1,29 @@ #!/usr/bin/env node -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const args = process.argv.slice(2); -const framework = args[0] || "next"; - -if (!["next", "react", "vite"].includes(framework)) { - console.log("Uso: npx github-automated-repos-example [next|react|vite]"); - process.exit(1); -} - -console.log(`πŸš€ Gerando exemplo para ${framework}...`); - -let sourceDir; -let targetDir; - -switch (framework) { - case "next": - sourceDir = path.join(__dirname, "../examples/nextjs/page.tsx"); - targetDir = path.join(process.cwd(), "src/app/projects/page.tsx"); - break; - case "react": - sourceDir = path.join(__dirname, "../examples/react/Project.jsx"); - targetDir = path.join(process.cwd(), "src/components/Project.jsx"); - break; - case "vite": - sourceDir = path.join(__dirname, "../examples/vite/Project.tsx"); - targetDir = path.join(process.cwd(), "src/components/Project.tsx"); - break; -} - -fs.mkdirSync(path.dirname(targetDir), { recursive: true }); - -fs.copyFileSync(sourceDir, targetDir); -console.log(`βœ… Exemplo criado em: ${targetDir}`); -console.log("πŸ’‘ Agora vocΓͺ pode acessar a pΓ‘gina no seu projeto Next.js em /projects"); +import { Command } from "commander"; +import chalk from "chalk"; +import initCommand from "../commands/init.js"; + +const program = new Command(); + +program + .name("github-automated-repos-cli") + .description("CLI oficial do github-automated-repos (only init)") + .version("1.0.0"); + +program + .command("init") + .description("Initialize the project: ensure dependency, install if needed and add example page/component") + .option("-y, --yes", "auto-confirm prompts (non-interactive)") + .action((opts) => { + initCommand + .run(opts) + .then(() => { + console.log(chalk.green("Init finished successfully.")); + }) + .catch((err) => { + console.error(chalk.red("Init failed:"), err.message || err); + process.exit(1); + }); + }); + +program.parse(process.argv); diff --git a/changelog-config.cjs b/changelog-config.cjs new file mode 100644 index 0000000..1b37016 --- /dev/null +++ b/changelog-config.cjs @@ -0,0 +1,107 @@ +module.exports = { + preset: "angular", + writerOpts: { + transform: (commit) => { + if (!commit.hash) return null; + + // πŸ”‘ Only user-relevant changes + const allowedTypes = ["feat", "fix"]; + + const knownScopes = [ + "cli", + "init", + "commands", + "interactive", + "utils", + "projectFile", + "pkgManager", + "framework", + "bin", + "pageExample", + "page", + "vite-template", + "next-template", + "github-actions", + "general", + ]; + + + if (!allowedTypes.includes(commit.type)) { + return null; + } + + const shortHash = commit.hash.substring(0, 7); + const hashLink = + `https://github.com/DIGOARTHUR/github-automated-repos-cli/commit/${commit.hash}`; + + let normalizedScope = commit.scope; + + if (normalizedScope) { + normalizedScope = normalizedScope + .replace(/files?/i, "projectFile") + .replace(/project[-_]?file/i, "projectFile") + .replace(/pkg[-_]?manager/i, "pkgManager") + .replace(/framework/i, "framework") + .replace(/interactive/i, "interactive") + .replace(/commands?/i, "commands") + .replace(/utils?/i, "utils") + .replace(/bin/i, "bin") + .replace(/init/i, "init") + .replace(/cli/i, "cli") + .replace(/page[-_]?example/i, "pageExample") + .replace(/vite[-_]?template/i, "vite-template") + .replace(/next[-_]?template/i, "next-template") + .replace(/github[-_]?actions?/i, "github-actions"); + } + + if (!normalizedScope || !knownScopes.includes(normalizedScope)) { + normalizedScope = "general"; + } + + return { + ...commit, + scope: normalizedScope, + shortHash, + hashLink, + }; + }, + + groupBy: "scope", + + commitGroupsSort: (a, b) => { + const order = [ + "cli", + "init", + "commands", + "interactive", + "projectFile", + "utils", + "pkgManager", + "framework", + "pageExample", + "page", + "vite-template", + "next-template", + "general", + ]; + return order.indexOf(a.title) - order.indexOf(b.title); + }, + + commitsSort: ["type", "subject"], + + headerPartial: + '\n# {{version}} ({{date}})\n\n', + + commitPartial: + "| [{{shortHash}}]({{hashLink}}) | {{type}} | {{subject}} |\n", + + mainTemplate: `{{> header}} +{{#each commitGroups}} +### {{title}} +| Commit | Type | Description | +|--------|------|-------------| +{{#each commits}}{{> commit}}{{/each}} + +{{/each}}`, + }, +}; diff --git a/commands/init.js b/commands/init.js new file mode 100644 index 0000000..a2e1a3b --- /dev/null +++ b/commands/init.js @@ -0,0 +1,140 @@ +// src/cli/init.js +import fs from "fs"; +import path from "path"; +import chalk from "chalk"; +import { fileURLToPath } from "url"; +import ora from "ora"; + +import detectFramework from "../utils/framework/detectFramework.js"; +import detectPackageManager from "../utils/pkgManager/detectPackageManager.js"; +import getInstallCommand from "../utils/pkgManager/getInstallCommand.js"; +import installDependency from "../utils/pkgManager/installDependency.js"; +import ensureDependencyExists from "../utils/pkgManager/ensureDependencyExists.js"; + +import doesProjectFileExist from "../utils/projectFile/doesProjectFileExist.js"; +import getUniqueNewProjectPath from "../utils/projectFile/getUniqueNewProjectPath.js"; +import createProjectFile from "../utils/projectFile/createProjectFile.js"; +import injectHookPlaceholders from "../utils/projectFile/injectHookPlaceholders.js"; + +import promptTargetAction from "./interactive/promptTargetAction.js"; +import askForUserAndKeyword from "./interactive/askForUserAndKeyword.js"; +import getLocalhostUrl from "../utils/getLocalhostUrl.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const examplesRoot = path.join(__dirname, "..", "pageExample"); + +async function run(opts = {}) { + const spinner = ora(); + + try { + + const projectRoot = process.cwd(); + const packageJsonPath = path.join(projectRoot, "package.json"); + + const hasPackageJson = fs.existsSync(packageJsonPath); + if (!hasPackageJson) { + throw new Error("No package.json found in the project root."); + } + + + const dependencyName = "github-automated-repos"; + + spinner.start(`Checking dependency: ${dependencyName}`); + const dependencyExists = ensureDependencyExists(projectRoot, dependencyName); + + if (!dependencyExists) { + const packageManager = detectPackageManager(projectRoot); + const installCommand = getInstallCommand(packageManager, dependencyName); + + spinner.start(`Installing ${dependencyName} using ${packageManager}...`); + const installedSuccessfully = installDependency(projectRoot, installCommand); + + if (!installedSuccessfully) { + throw new Error("Failed to install dependency."); + } + } + + spinner.succeed(`${dependencyName} ready.`); + + + const framework = detectFramework(projectRoot); + + let srcExamplePath; + let targetPath; + + switch (framework) { + case "next-app": + srcExamplePath = path.join(examplesRoot, "Project.next.tsx"); + targetPath = path.join(projectRoot, "app", "projects", "page.tsx"); + break; + + case "next-pages": + srcExamplePath = path.join(examplesRoot, "Project.next.tsx"); + targetPath = path.join(projectRoot, "pages", "projects", "index.tsx"); + break; + + case "vite": + srcExamplePath = path.join(examplesRoot, "Project.vite.tsx"); + targetPath = path.join(projectRoot, "src", "components", "Project.tsx"); + break; + + default: + srcExamplePath = path.join(examplesRoot, "Project.react.tsx"); + targetPath = path.join(projectRoot, "src", "components", "Project.tsx"); + } + + + const projectFileExists = doesProjectFileExist(targetPath); + + if (projectFileExists) { + const targetDecision = await promptTargetAction(targetPath, spinner, opts); + const { action } = targetDecision; + + if (action === "cancel") { + return; + } + + if (action === "create_new") { + const newTargetPath = getUniqueNewProjectPath(targetPath); + targetPath = newTargetPath; + } + } + + + const userAnswers = await askForUserAndKeyword(opts); + + + spinner.start("Preparing projects page template..."); + const templateContent = fs.readFileSync(srcExamplePath, "utf8"); + + const finalContent = injectHookPlaceholders( + templateContent, + userAnswers + ); + + spinner.succeed("Template ready."); + + + spinner.start("Creating project file..."); + const createdFilePath = createProjectFile(targetPath, finalContent); + + spinner.succeed( + chalk.green(`File created at: ${createdFilePath}`) + ); + + + const projectUrl = getLocalhostUrl(framework); + + console.log("\n" + chalk.green("✨ Your project page is ready!")); + console.log(chalk.cyan(`πŸ”— ${projectUrl}\n`)); + + } catch (err) { + spinner.fail("Error during execution."); + console.error(chalk.red("❌ Error:"), err.message || err); + process.exitCode = 1; + } finally { + spinner.stop(); + } +} + +export default { run }; diff --git a/commands/interactive/askForUserAndKeyword.js b/commands/interactive/askForUserAndKeyword.js new file mode 100644 index 0000000..c938212 --- /dev/null +++ b/commands/interactive/askForUserAndKeyword.js @@ -0,0 +1,43 @@ +import inquirer from "inquirer"; +/** + * @description Prompts the user for GitHub username and keyword unless they were provided via CLI flags. + * @param {{ username?: string, keyword?: string }} opts - Optional values passed through CLI flags. + * @returns {Promise<{ username: string, keyword: string }>} The resolved username and keyword. + */ +export default async function askForUserAndKeyword(opts = {}) { + const questions = []; + + if (!opts.username) { + questions.push({ + type: "input", + name: "username", + message: "GitHub username:", + validate: (value) => + value.trim() ? true : "The username cannot be empty.", + }); + } + + if (!opts.keyword) { + questions.push({ + type: "input", + name: "keyword", + message: "Keyword to filter (e.g. 'attached'):", + validate: (value) => + value.trim() ? true : "The keyword cannot be empty.", + }); + } + + if (questions.length === 0) { + return { + username: opts.username, + keyword: opts.keyword, + }; + } + + const answers = await inquirer.prompt(questions); + + return { + username: opts.username || answers.username, + keyword: opts.keyword || answers.keyword, + }; +} diff --git a/commands/interactive/promptTargetAction.js b/commands/interactive/promptTargetAction.js new file mode 100644 index 0000000..6b03dbb --- /dev/null +++ b/commands/interactive/promptTargetAction.js @@ -0,0 +1,30 @@ +// src/cli/interactive/promptTargetAction.js +import chalk from "chalk"; +import prompts from "prompts"; + +export default async function promptTargetAction(targetPath, spinner, opts = {}) { + if (spinner?.stop) spinner.stop(); + + if (opts.yes) { + console.log(chalk.yellow(`--yes detected: will overwrite ${targetPath}\n`)); + return { action: "overwrite" }; + } + + const { action } = await prompts({ + type: "select", + name: "action", + message: `Target file already exists:\n${targetPath}\nWhat would you like to do?`, + choices: [ + { title: "Overwrite", value: "overwrite" }, + { title: "Create a new file", value: "create_new" }, + { title: "Cancel", value: "cancel" } + ] + }); + + if (!action || action === "cancel") { + console.log(chalk.yellow("\nOperation cancelled.\n")); + return { action: "cancel" }; + } + + return { action }; +} diff --git a/examples/nextjs/page.tsx b/examples/nextjs/page.tsx deleted file mode 100644 index 89dda9f..0000000 --- a/examples/nextjs/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client"; -import React from "react"; - -"use client"; -import { useGitHubAutomatedRepos, StackIcons, StackLabels } from "github-automated-repos"; - - -export default function Home() { - - const { data, isLoading } = useGitHubAutomatedRepos('digoarthur', 'attached'); - if (isLoading) return

Loading...

; - - return ( -
- Header - - {data?.map((repo) => ( -
-

{repo.name}

- -
- {repo.banner.map((url) => Banner)} -
- -
- {repo.topics.map((stackName) => ( - - - - ))} -
- -

{repo.description}

- -
- πŸ”— Homepage - πŸ”— Repository -
-
- ))} - - -
- ); -} diff --git a/package.json b/package.json index d4bc982..e2a09b7 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,33 @@ { "name": "github-automated-repos-cli", "version": "1.0.0", - "description": "CLI para gerar pΓ‘gina de exemplo usando github-automated-repos em projetos Next.js, React ou Vite", + "description": "CLI para gerar pΓ‘gina de exemplo usando github-automated-repos em projetos Next.js, React ou Vite.", + "type": "module", "bin": { - "github-automated-repos-example": "bin/index.js" + "github-automated-repos-cli": "./bin/index.js" }, - "type": "module", "scripts": { - "start": "node bin/index.js" + "localTest": "npm link", + "dev:setup": "git update-index --skip-worktree CHANGELOG.md || true", + "start": "node ./bin/index.js", + "changelog": "conventional-changelog --config changelog-config.cjs -i CHANGELOG.md -s -r 0" + }, - "author": "Seu Nome", + "author": "Digo Arthur", "license": "MIT", - "dependencies": {}, + "dependencies": { + "chalk": "^5.6.2", + "commander": "^14.0.2", + "inquirer": "^13.0.1", + "ora": "^9.0.0", + "prompts": "^2.4.2" + }, "files": [ "bin", - "examples" - ] + "example", + "README.md" + ], + "devDependencies": { + "conventional-changelog-cli": "^5.0.0" + } } diff --git a/pageExample/Project.next.tsx b/pageExample/Project.next.tsx new file mode 100644 index 0000000..6583e73 --- /dev/null +++ b/pageExample/Project.next.tsx @@ -0,0 +1,81 @@ +"use client"; +import { useGitHubAutomatedRepos, StackIcons, StackLabels } from "github-automated-repos"; + +export default function Projects() { + + const { data, isLoading } = useGitHubAutomatedRepos("__GITHUB_USERNAME__", "__KEYWORD__"); + if (isLoading) return

Loading...

; + + return ( + +
+ Header + +
+ + {/* CARD 1 */} + +
1
hook icon

Set up the Hook

In the code, adjust the githubUsername and keyword in the hook β€” the keyword is chosen by you.

useGitHubAutomatedRepos (
"githubUsername" , "your-keyword" )
+ + {/* CARD 2 */} + +
2
github icon

Select Your Repositories

Don't forget to fill in:

  • Repository descriptioninfo
  • Project bannerinfo
    /public/bannerXYZ.png
  • Technology logos (Topics)info
react-icons .NET Core MySQL MongoDB Linux Vue Vite TypeScript Tailwind Swift
+ {/* CARD 3 */} + +
3
key icon

Add the Keyword

In your repository:
About β†’ βš™οΈ β†’ Topics

Topics (separate with spaces)
keywordX

βœ… Check out the cards below!

+ {/* === πŸ”Ή END OF CARDS === */} + + + {data?.map((repo) => ( +
+

{repo.name}

+ +
+ {repo.banner.map((url) => Banner)} +
+ +
+ {repo.topics.map((stackName) => ( + + + + ))} +
+ +

{repo.description}

+ +
+ πŸ”— Homepage + πŸ”— Repository +
+
+ ))} + + +
+ ); +} diff --git a/pageExample/Project.vite.tsx b/pageExample/Project.vite.tsx new file mode 100644 index 0000000..2125691 --- /dev/null +++ b/pageExample/Project.vite.tsx @@ -0,0 +1,80 @@ +import { useGitHubAutomatedRepos, StackIcons, StackLabels } from "github-automated-repos"; + +export default function Projects() { + + const { data, isLoading } = useGitHubAutomatedRepos("__GITHUB_USERNAME__", "__KEYWORD__"); + if (isLoading) return

Loading...

; + + return ( + +
+ Header + +
+ + {/* CARD 1 */} + +
1
hook icon

Set up the Hook

In the code, adjust the githubUsername and keyword in the hook β€” the keyword is chosen by you.

useGitHubAutomatedRepos (
"githubUsername" , "your-keyword" )
+ + {/* CARD 2 */} + +
2
github icon

Select Your Repositories

Don't forget to fill in:

  • Repository descriptioninfo
  • Project bannerinfo
    /public/bannerXYZ.png
  • Technology logos (Topics)info
react-icons .NET Core MySQL MongoDB Linux Vue Vite TypeScript Tailwind Swift
+ {/* CARD 3 */} + +
3
key icon

Add the Keyword

In your repository:
About β†’ βš™οΈ β†’ Topics

Topics (separate with spaces)
keywordX

βœ… Check out the cards below!

+ {/* === πŸ”Ή END OF CARDS === */} + + + {data?.map((repo) => ( +
+

{repo.name}

+ +
+ {repo.banner.map((url) => Banner)} +
+ +
+ {repo.topics.map((stackName) => ( + + + + ))} +
+ +

{repo.description}

+ +
+ πŸ”— Homepage + πŸ”— Repository +
+
+ ))} + + +
+ ); +} diff --git a/utils/framework/detectFramework.js b/utils/framework/detectFramework.js new file mode 100644 index 0000000..0ec8af3 --- /dev/null +++ b/utils/framework/detectFramework.js @@ -0,0 +1,21 @@ +import fs from "fs"; +import path from "path"; + +/** + * @description Detects whether the project is using Next.js (app/pages), Vite, or plain React. + * @param {string} projectRoot - The absolute path of the project root. + * @returns {"next-app" | "next-pages" | "vite" | "react"} The detected framework. + */ +export default function detectFramework(projectRoot) { + const appDir = path.join(projectRoot, "app"); + const pagesDir = path.join(projectRoot, "pages"); + const isVite = + fs.existsSync(path.join(projectRoot, "vite.config.js")) || + fs.existsSync(path.join(projectRoot, "vite.config.ts")); + + if (fs.existsSync(appDir)) return "next-app"; + if (fs.existsSync(pagesDir)) return "next-pages"; + if (isVite) return "vite"; + + return "react"; // fallback +} diff --git a/utils/getLocalhostUrl.js b/utils/getLocalhostUrl.js new file mode 100644 index 0000000..fe2b6a8 --- /dev/null +++ b/utils/getLocalhostUrl.js @@ -0,0 +1,24 @@ +/** + * @description Returns the correct localhost URL based on the detected framework. + * @param {"next-app" | "next-pages" | "vite" | "react" | string} framework - The detected framework type. + * @param {string} route - The route to append to the localhost URL. + * @returns {string} The complete localhost URL for the given framework. + */ +export default function getLocalhostUrl(framework, route = "/projects") { + let port = 3000; // default + + switch (framework) { + case "vite": + port = 5173; + break; + + case "next-app": + case "next-pages": + case "react": + default: + port = 3000; + break; + } + + return `http://localhost:${port}${route}`; +} diff --git a/utils/pkgManager/detectPackageManager.js b/utils/pkgManager/detectPackageManager.js new file mode 100644 index 0000000..9b3a6b1 --- /dev/null +++ b/utils/pkgManager/detectPackageManager.js @@ -0,0 +1,13 @@ +import fs from "fs"; +import path from "path"; + +/** + * @description Detects which package manager is being used in the project (pnpm, yarn, or npm). + * @param {string} projectRoot - Absolute path of the project root. + * @returns {"pnpm" | "yarn" | "npm"} The detected package manager. + */ +export default function detectPackageManager(projectRoot) { + if (fs.existsSync(path.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm"; + if (fs.existsSync(path.join(projectRoot, "yarn.lock"))) return "yarn"; + return "npm"; +} diff --git a/utils/pkgManager/ensureDependencyExists.js b/utils/pkgManager/ensureDependencyExists.js new file mode 100644 index 0000000..68de293 --- /dev/null +++ b/utils/pkgManager/ensureDependencyExists.js @@ -0,0 +1,26 @@ +import fs from "fs"; +import path from "path"; + +/** + * @description Checks whether a given dependency exists inside the project's package.json. + * @param {string} projectRoot - Absolute path of the project root. + * @param {string} packageName - Name of the dependency to verify. + * @returns {boolean} True if the dependency is listed, otherwise false. + */ +export default function ensureDependencyExists(projectRoot, packageName) { + const pkgPath = path.join(projectRoot, "package.json"); + if (!fs.existsSync(pkgPath)) { + return false; + } + + try { + const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + const present = + (pkg.dependencies && pkg.dependencies[packageName]) || + (pkg.devDependencies && pkg.devDependencies[packageName]); + + return Boolean(present); + } catch (err) { + return false; + } +} diff --git a/utils/pkgManager/getInstallCommand.js b/utils/pkgManager/getInstallCommand.js new file mode 100644 index 0000000..462730c --- /dev/null +++ b/utils/pkgManager/getInstallCommand.js @@ -0,0 +1,16 @@ +/** + * @description Returns the correct install command based on the detected package manager. + * @param {"npm" | "yarn" | "pnpm" | string} packageManager - The package manager detected in the user's project. + * @param {string} packageName - The dependency name that should be installed. + * @returns {string} The full command that should be executed to install the dependency. + */ +export default function getInstallCommand(packageManager, packageName) { + switch (packageManager) { + case "pnpm": + return `pnpm add ${packageName}`; + case "yarn": + return `yarn add ${packageName}`; + default: + return `npm install ${packageName} --save`; + } +} diff --git a/utils/pkgManager/installDependency.js b/utils/pkgManager/installDependency.js new file mode 100644 index 0000000..c345b7e --- /dev/null +++ b/utils/pkgManager/installDependency.js @@ -0,0 +1,23 @@ +import { execSync } from "child_process"; +import chalk from "chalk"; + +/** + * @description Executes the install command using the detected package manager and installs the required dependency in the project. + * @param {string} projectRoot - Absolute path of the user's project where the installation should run. + * @param {string} installCommand - Full install command (e.g., "npm install X", "yarn add X", "pnpm add X"). + * @returns {boolean} Returns true if installation succeeded, otherwise false. + */ +export default function installDependency(projectRoot, installCommand) { + try { + console.log(chalk.gray(`Running in ${projectRoot}:`)); + console.log(chalk.yellow(`> ${installCommand}`)); + + execSync(installCommand, { stdio: "inherit", cwd: projectRoot }); + + console.log(chalk.green("βœ” Dependency installation finished.")); + return true; + } catch (err) { + console.error(chalk.red("❌ Install command failed:"), err.message); + return false; + } +} diff --git a/utils/projectFile/createProjectFile.js b/utils/projectFile/createProjectFile.js new file mode 100644 index 0000000..2ccd91e --- /dev/null +++ b/utils/projectFile/createProjectFile.js @@ -0,0 +1,9 @@ +// src/utils/projectFile/createProjectFile.js +import fs from "fs"; +import path from "path"; + +export default function createProjectFile(targetPath, content) { + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.writeFileSync(targetPath, content, "utf8"); + return targetPath; +} diff --git a/utils/projectFile/doesProjectFileExist.js b/utils/projectFile/doesProjectFileExist.js new file mode 100644 index 0000000..6624986 --- /dev/null +++ b/utils/projectFile/doesProjectFileExist.js @@ -0,0 +1,12 @@ + +import fs from "fs"; + + +export default function doesProjectFileExist(targetPath) { + try { + return fs.existsSync(targetPath); + } catch (err) { + + return false; + } +} diff --git a/utils/projectFile/getUniqueNewProjectPath.js b/utils/projectFile/getUniqueNewProjectPath.js new file mode 100644 index 0000000..ce46e6a --- /dev/null +++ b/utils/projectFile/getUniqueNewProjectPath.js @@ -0,0 +1,19 @@ + +import fs from "fs"; +import path from "path"; + +export default function getUniqueNewProjectPath(filePath) { + const dir = path.dirname(filePath); + const ext = path.extname(filePath); + const base = path.basename(filePath, ext); + + let candidate = path.join(dir, `New${base}${ext}`); + let counter = 1; + + while (fs.existsSync(candidate)) { + candidate = path.join(dir, `New${base}${counter}${ext}`); + counter++; + } + + return candidate; +} diff --git a/utils/projectFile/injectHookPlaceholders.js b/utils/projectFile/injectHookPlaceholders.js new file mode 100644 index 0000000..68e5ad2 --- /dev/null +++ b/utils/projectFile/injectHookPlaceholders.js @@ -0,0 +1,11 @@ +/** + * @description Replaces template placeholders inside a string with user-provided values. + * @param {string} content - The template text where replacements will occur. + * @param {{ username: string, keyword: string }} params - Values to inject into placeholders. + * @returns {string} The updated content with placeholders replaced. + */ +export default function injectHookPlaceholders(content, { username, keyword }) { + return content + .replaceAll("__GITHUB_USERNAME__", username) + .replaceAll("__KEYWORD__", keyword); +}