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..0fc01b6 --- /dev/null +++ b/.github/workflows/validate-version-pr.yml @@ -0,0 +1,54 @@ +name: Validate version + +on: + pull_request: + branches: + - main + push: + branches: + - '**.x' +jobs: + validate-version: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Validate package.json version against branch + run: | + SOURCE_BRANCH="${{ github.head_ref }}" + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "" + echo "π Version validation" + echo "----------------------" + echo "Source branch: $SOURCE_BRANCH" + echo "package.json version: $PACKAGE_VERSION" + echo "" + + # Extract expected major.minor from branch (e.g. 1.2.x -> 1.2) + EXPECTED_PREFIX=$(echo "$SOURCE_BRANCH" | sed 's/\.x//') + + if [[ "$PACKAGE_VERSION" != "$EXPECTED_PREFIX"* ]]; then + echo "β Version validation failed" + echo "" + echo "Expected package.json version to start with:" + echo " - $EXPECTED_PREFIX." + echo "" + echo "Found:" + echo " - version: $PACKAGE_VERSION" + echo "" + echo "π οΈ Please fix the following:" + echo " - Update the version in package.json to match the current branch version you are working on" + echo "" + echo "π‘ As well as the package and branch version, be sure to check the code version published in NPM to follow a correct sequence." + exit 1 + fi + + echo "β Version validation passed" 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 ( -{repo.description}
- -Loading...
; + + return ( + +
In the code, adjust the githubUsername and keyword in the hook β the keyword is chosen by you.
useGitHubAutomatedRepos (
"githubUsername" , "your-keyword" )
Don't forget to fill in:
In your repository:
About β βοΈ β Topics
β Check out the cards below!
{repo.description}
+ +Loading...
; + + return ( + +
In the code, adjust the githubUsername and keyword in the hook β the keyword is chosen by you.
useGitHubAutomatedRepos (
"githubUsername" , "your-keyword" )
Don't forget to fill in:
In your repository:
About β βοΈ β Topics
β Check out the cards below!
{repo.description}
+ +