From c3e8271e3a1b1243f90c131246ab5e918c0e0a72 Mon Sep 17 00:00:00 2001 From: Wooseok Jeon <58899677+wsk0715@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:12:41 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=86=A0=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + commands/branch.js | 20 +++++ commands/config.js | 134 +++++++++++++++++++++++++++++++++ commands/issue.js | 23 ++++++ commands/label/create.js | 25 ++++++ commands/label/index.js | 22 ++++++ commands/label/reset.js | 16 ++++ commands/repo.js | 95 +++++++++++++++++++++++ config/env.js | 72 ++++++++++++++++++ config/env.json | 4 + config/env.json.example | 4 + data/labels.json | 159 +++++++++++++++++++++++++++++++++++++++ index.js | 45 +++++++++++ libs/git.js | 61 +++++++++++++++ libs/octokit.js | 15 ++++ package.json | 16 ++++ utils/prompt.js | 131 ++++++++++++++++++++++++++++++++ utils/system/cmd.js | 15 ++++ utils/system/fs.js | 14 ++++ utils/system/path.js | 21 ++++++ 20 files changed, 896 insertions(+) create mode 100644 .gitignore create mode 100644 commands/branch.js create mode 100644 commands/config.js create mode 100644 commands/issue.js create mode 100644 commands/label/create.js create mode 100644 commands/label/index.js create mode 100644 commands/label/reset.js create mode 100644 commands/repo.js create mode 100644 config/env.js create mode 100644 config/env.json create mode 100644 config/env.json.example create mode 100644 data/labels.json create mode 100644 index.js create mode 100644 libs/git.js create mode 100644 libs/octokit.js create mode 100644 package.json create mode 100644 utils/prompt.js create mode 100644 utils/system/cmd.js create mode 100644 utils/system/fs.js create mode 100644 utils/system/path.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3867341 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +package-lock.json +node_modules +.env + diff --git a/commands/branch.js b/commands/branch.js new file mode 100644 index 0000000..1e03178 --- /dev/null +++ b/commands/branch.js @@ -0,0 +1,20 @@ +const git = require("../libs/git"); +const prompt = require("../utils/prompt"); + +module.exports = async function branch(cmd) { + if (cmd.create) { + const { issueNumber, prefix, branchName, baseBranch } = await prompt.createBranch(); + + // TODO: 레포 확인, 이슈 확인 등 유효성 관련 로직 처리 필요. + const newBranchName = `${prefix}/${issueNumber}-${branchName}`; + + console.log(`\n📌 브랜치명: ${newBranchName}`); + + // TODO: 브랜치 존재하지 않을 경우 에러 처리 + git.fetch(baseBranch); + git.checkoutWithNewBranch(newBranchName, `origin/${baseBranch}`); + git.push(newBranchName); + + console.log("✅ 브랜치 생성 완료!"); + } +}; diff --git a/commands/config.js b/commands/config.js new file mode 100644 index 0000000..03b4fd5 --- /dev/null +++ b/commands/config.js @@ -0,0 +1,134 @@ +const env = require("../config/env"); +const octokit = require("../libs/octokit"); +const prompt = require("../utils/prompt"); + +async function configCommand(options) { + // config 명령어는 토큰이 없어도 실행 가능 + await env.load(false); + if (options.token) { + await setGitHubToken(); + } else if (options.path) { + await setClonePath(); + } else if (options.show) { + showCurrentConfig(); + } else { + await interactiveConfig(); + } +} + +async function setGitHubToken() { + console.log("GitHub Token을 설정합니다. 토큰이 없다면 토큰을 생성해주세요."); + console.log("토큰은 repo, user 권한이 필요합니다. (https://github.com/settings/tokens/new)"); + console.log("토큰을 입력해주세요."); + + const { githubToken } = await prompt.githubToken(); + + // 토큰 유효성 검증 + try { + const { Octokit } = require("@octokit/rest"); + const tempOctokit = new Octokit({ + auth: githubToken, + }); + + const { data: user } = await tempOctokit.rest.users.getAuthenticated(); + + env.GITHUB_TOKEN = githubToken; + env.save(); + + console.log(`✅ 토큰이 성공적으로 설정되었습니다. (사용자: ${user.login})`); + } catch (error) { + console.error("❌ 유효하지 않은 토큰입니다. 다시 확인해주세요."); + console.error("오류 상세:", error.message); + if (error.status) { + console.error("HTTP 상태:", error.status); + } + process.exit(1); + } +} + +async function setClonePath() { + console.log("저장소 클론 기본 경로를 설정합니다."); + console.log(`현재 설정: ${env.DEFAULT_CLONE_PATH}`); + + const inquirer = require("inquirer"); + const { clonePath } = await inquirer.prompt([ + { + type: "input", + name: "clonePath", + message: "새로운 클론 경로를 입력하세요:", + default: env.DEFAULT_CLONE_PATH, + validate: (input) => { + if (!input.trim()) return "경로를 입력해주세요."; + return true; + } + } + ]); + + env.DEFAULT_CLONE_PATH = clonePath.trim(); + env.save(); + console.log(`✅ 클론 기본 경로가 설정되었습니다: ${env.DEFAULT_CLONE_PATH}`); +} + +function showCurrentConfig() { + const git = require("../libs/git"); + + console.log("현재 설정:"); + console.log(`GitHub 토큰: ${env.GITHUB_TOKEN ? "설정됨" : "설정되지 않음"}`); + console.log(`Git remote owner: ${git.currentOwner() || "Git 저장소가 아니거나 GitHub remote가 없음"}`); + console.log(`클론 기본 경로: ${env.DEFAULT_CLONE_PATH}`); +} + +async function interactiveConfig() { + console.log("GitHub DevKit 설정을 진행합니다."); + + const inquirer = require("inquirer"); + const { configType } = await inquirer.prompt([ + { + type: "list", + name: "configType", + message: "설정할 항목을 선택하세요:", + choices: [ + { name: "GitHub 토큰 설정", value: "token" }, + { name: "클론 기본 경로 설정", value: "path" }, + { name: "현재 설정 보기", value: "show" }, + { name: "토큰 재설정", value: "reset" } + ] + } + ]); + + switch (configType) { + case "token": + await setGitHubToken(); + break; + case "path": + await setClonePath(); + break; + case "show": + showCurrentConfig(); + break; + case "reset": + await resetConfig(); + break; + } +} + +async function resetConfig() { + const inquirer = require("inquirer"); + const { confirm } = await inquirer.prompt([ + { + type: "confirm", + name: "confirm", + message: "GitHub 토큰을 초기화하시겠습니까?", + default: false + } + ]); + + if (confirm) { + env.GITHUB_TOKEN = ""; + env.save(); + console.log("✅ GitHub 토큰이 초기화되었습니다."); + console.log("파일이 업데이트되었습니다:", require("../utils/system/path").resolveAbsPath("./config/env.json")); + } +} + +module.exports = configCommand; \ No newline at end of file diff --git a/commands/issue.js b/commands/issue.js new file mode 100644 index 0000000..29d4618 --- /dev/null +++ b/commands/issue.js @@ -0,0 +1,23 @@ +const octokit = require("../libs/octokit"); +const prompt = require("../utils/prompt"); +const env = require("../config/env"); +const git = require("../libs/git"); + +module.exports = async function issue(cmd) { + if (cmd.create) { + const github = await octokit.getInstance(); + + const username = env.GITHUB_USERNAME; + const repo = git.currentRepo() || (await prompt.repo()).repo; + const { issueTitle, issueBody } = await prompt.issue(); + + const issue = await github.issues.create({ + owner: username, + repo, + title: issueTitle, + body: issueBody, + }); + + console.log("✅ 이슈 생성 완료:", issue.data.html_url); + } +}; diff --git a/commands/label/create.js b/commands/label/create.js new file mode 100644 index 0000000..a00065b --- /dev/null +++ b/commands/label/create.js @@ -0,0 +1,25 @@ +const octokit = require("../../libs/octokit"); + +module.exports = { + async createLabels(owner, repo, selected) { + const github = await octokit.getInstance(); + for (const label of selected) { + try { + await github.issues.createLabel({ + owner, + repo, + name: label.name, + color: label.color, + description: label.description, + }); + console.log(`✅ 라벨 생성됨: ${label.name}`); + } catch (e) { + if (e.status === 422) { + console.log(`⚠️ 라벨이 이미 존재합니다: ${label.name}`); + } else { + console.error(`❌ 라벨 생성 실패: ${label.name}`, e.message); + } + } + } + }, +}; diff --git a/commands/label/index.js b/commands/label/index.js new file mode 100644 index 0000000..e9794f1 --- /dev/null +++ b/commands/label/index.js @@ -0,0 +1,22 @@ +const { resetLabels } = require("./reset"); +const { createLabels } = require("./create"); +const prompt = require("../../utils/prompt"); +const git = require("../../libs/git"); +const env = require("../../config/env"); + +module.exports = async function label(cmd) { + await env.load(); + const repo = git.currentRepo() || (await prompt.repo()).repo; + const selected = (await prompt.selectLabels()).selected; + + const owner = env.GITHUB_USERNAME; + if (cmd.reset) { + console.log(`🧹 기존 라벨 제거 중...`); + await resetLabels(owner, repo); + console.log("🎉 기존 라벨 제거 완료!"); + } + + console.log("🏷️ 라벨 생성 중..."); + await createLabels(owner, repo, selected); + console.log("🎉 라벨 생성 작업 완료"); +}; diff --git a/commands/label/reset.js b/commands/label/reset.js new file mode 100644 index 0000000..a6fd8fc --- /dev/null +++ b/commands/label/reset.js @@ -0,0 +1,16 @@ +const octokit = require("../../libs/octokit"); + +module.exports = { + async resetLabels(owner, repo) { + const github = await octokit.getInstance(); + const existingLabels = await github.issues.listLabelsForRepo({ owner, repo }); + for (const label of existingLabels.data) { + try { + await github.issues.deleteLabel({ owner, repo, name: label.name }); + console.log(`❌ 삭제됨: ${label.name}`); + } catch (e) { + console.error(`삭제 실패: ${label.name}`, e.message); + } + } + }, +}; diff --git a/commands/repo.js b/commands/repo.js new file mode 100644 index 0000000..c17294f --- /dev/null +++ b/commands/repo.js @@ -0,0 +1,95 @@ +const octokit = require("../libs/octokit"); +const prompt = require("../utils/prompt"); +const git = require("../libs/git"); +const env = require("../config/env"); +const path = require("path"); + +module.exports = async function repo(command) { + if (command.create) { + await env.getInstance(); + + const { name, description, isPrivate } = await prompt.createRepo(); + + // 저장 경로 확인 및 설정 + const targetPath = await confirmClonePath(name); + + // GitHub 저장소 생성 + const github = await octokit.getInstance(); + const response = await github.repos.createForAuthenticatedUser({ + name, + description, + private: isPrivate, + }); + + // 지정된 경로에 클론 + cloneToPath(response.data.html_url, targetPath); + console.log("\n✅ 저장소 생성 완료!"); + console.log(`📁 저장소 위치: ${targetPath}`); + } +}; + +async function confirmClonePath(repoName) { + const defaultPath = path.join(env.DEFAULT_CLONE_PATH, repoName); + + console.log(`\n📁 저장소가 다음 위치에 저장됩니다:`); + console.log(` ${defaultPath}`); + + const inquirer = require("inquirer"); + const { useDefault } = await inquirer.prompt([ + { + type: "confirm", + name: "useDefault", + message: "이 경로에 저장하시겠습니까?", + default: true + } + ]); + + if (useDefault) { + return defaultPath; + } + + const { customPath } = await inquirer.prompt([ + { + type: "input", + name: "customPath", + message: "저장할 경로를 입력하세요 (공백 시 기본 경로):", + validate: (input) => { + if (!input.trim()) return true; // 공백 허용 (기본 경로 사용) + return true; + }, + filter: (input) => { + if (!input.trim()) return defaultPath; + // 상대 경로면 절대 경로로 변환 + return path.isAbsolute(input) ? path.join(input, repoName) : path.join(process.cwd(), input, repoName); + } + } + ]); + + return customPath; +} + +function cloneToPath(repoUrl, targetPath) { + const fs = require("fs"); + const parentDir = path.dirname(targetPath); + const repoName = path.basename(targetPath); + + // 부모 디렉토리가 없으면 생성 + if (!fs.existsSync(parentDir)) { + fs.mkdirSync(parentDir, { recursive: true }); + console.log(`📁 디렉토리 생성: ${parentDir}`); + } + + // 부모 디렉토리로 이동하여 클론 + const originalCwd = process.cwd(); + + try { + process.chdir(parentDir); + git.clone(repoUrl); + + // 클론된 디렉토리로 이동 + process.chdir(repoName); + } catch (error) { + process.chdir(originalCwd); + throw error; + } +} diff --git a/config/env.js b/config/env.js new file mode 100644 index 0000000..fa89b7f --- /dev/null +++ b/config/env.js @@ -0,0 +1,72 @@ +const { readFile, writeFile } = require("../utils/system/fs"); +const { resolveAbsPath } = require("../utils/system/path"); +const prompt = require("../utils/prompt"); +const git = require("../libs/git"); +const fs = require("fs"); + +const ENV_PATH = resolveAbsPath("./config/env.json"); + +const env = { + GITHUB_TOKEN: "", + GITHUB_USERNAME: "", + DEFAULT_CLONE_PATH: "", + _loaded: false, + + async load(requireToken = true) { + if (this._loaded) return this; + + this._loaded = true; + + try { + let loaded = {}; + + if (fs.existsSync(ENV_PATH)) { + loaded = JSON.parse(readFile(ENV_PATH)); + } + + this.GITHUB_TOKEN = loaded.GITHUB_TOKEN || ""; + this.DEFAULT_CLONE_PATH = loaded.DEFAULT_CLONE_PATH || process.cwd(); + } catch (error) { + // 파일 파싱 오류 시 기본값 사용 + this.GITHUB_TOKEN = ""; + this.DEFAULT_CLONE_PATH = process.cwd(); + } + + if (!this.GITHUB_TOKEN && requireToken) { + console.log("GitHub 토큰이 존재하지 않습니다."); + console.log("다음 명령어로 토큰을 설정해주세요: gdk config -t"); + process.exit(1); + } + + // 토큰이 있을 때만 git remote에서 owner 정보 추출 + if (this.GITHUB_TOKEN) { + this.GITHUB_USERNAME = git.currentOwner(); + + if (!this.GITHUB_USERNAME) { + console.log("Git remote에서 GitHub owner 정보를 찾을 수 없습니다."); + console.log("GitHub 저장소에서 실행하거나 owner 정보를 수동으로 입력해주세요."); + this.GITHUB_USERNAME = (await prompt.githubUsername()).githubUsername; + } + } + + // 설정이 변경되었으면 저장 (최초 로딩 시에도 기본값 저장) + this.save(); + return this; + }, + + async getInstance() { + return await this.load(); + }, + + save() { + const config = { + GITHUB_TOKEN: this.GITHUB_TOKEN, + DEFAULT_CLONE_PATH: this.DEFAULT_CLONE_PATH, + }; + + const jsonContent = JSON.stringify(config, null, 2); + writeFile(ENV_PATH, jsonContent); + }, +}; + +module.exports = env; diff --git a/config/env.json b/config/env.json new file mode 100644 index 0000000..8ed80cc --- /dev/null +++ b/config/env.json @@ -0,0 +1,4 @@ +{ + "GITHUB_TOKEN": "", + "GITHUB_OWNER": "" +} diff --git a/config/env.json.example b/config/env.json.example new file mode 100644 index 0000000..fe4835e --- /dev/null +++ b/config/env.json.example @@ -0,0 +1,4 @@ +{ + "GITHUB_TOKEN": "your_github_personal_access_token_here", + "DEFAULT_CLONE_PATH": "/your/preferred/clone/path" +} \ No newline at end of file diff --git a/data/labels.json b/data/labels.json new file mode 100644 index 0000000..7faac5c --- /dev/null +++ b/data/labels.json @@ -0,0 +1,159 @@ +[ + { + "name": "Needs-Triage :mag:", + "color": "F9E5A4", + "description": "분류 대기중", + "order": 1, + "default": true + }, + { + "name": "Hotfix :fire:", + "color": "C0392B", + "description": "치명적인 버그(긴급 수정 필요)", + "order": 11, + "default": true + }, + { + "name": "Bug :bug:", + "color": "E57373", + "description": "치명적이지 않은 버그", + "order": 12, + "default": true + }, + { + "name": "ASAP :rotating_light:", + "color": "F4D03F", + "description": "우선순위가 높은 작업", + "order": 13, + "default": true + }, + + { + "name": "Status: In Progress :hourglass_flowing_sand:", + "color": "91C9F7", + "description": "작업 진행 중", + "order": 101, + "default": true + }, + { + "name": "Status: Paused :pause_button:", + "color": "C1C1C1", + "description": "작업이 일시 중단됨", + "order": 111, + "default": true + }, + { + "name": "Status: Blocked :no_entry:", + "color": "F08A8A", + "description": "다른 작업/요인에 의해 막힘", + "order": 112, + "default": true + }, + { + "name": "Status: Done :white_check_mark:", + "color": "89D07B", + "description": "작업 완료됨", + "order": 121, + "default": true + }, + { + "name": "Status: Aborted :x:", + "color": "C87972", + "description": "작업 취소됨(더 이상 진행하지 않음)", + "order": 131, + "default": true + }, + + { + "name": "Type: Documentation :memo:", + "color": "E3E3E3", + "description": "문서 관련 작업", + "order": 201, + "default": true + }, + { + "name": "Type: UI/UX :art:", + "color": "B4E49A", + "description": "UI/UX 관련 작업", + "order": 211, + "default": true + }, + { + "name": "Type: Feature :sparkles:", + "color": "B7D4EC", + "description": "새로운 기능 추가", + "order": 212, + "default": true + }, + { + "name": "Type: Enhancement :zap:", + "color": "CDDCEF", + "description": "기존 기능 개선", + "order": 213, + "default": true + }, + { + "name": "Type: Refactor :recycle:", + "color": "93D0F5", + "description": "코드 리팩터링", + "order": 221, + "default": true + }, + { + "name": "Type: Fix :bug:", + "color": "E68A92", + "description": "버그 수정", + "order": 222, + "default": true + }, + { + "name": "Type: Test :test_tube:", + "color": "D6A7DE", + "description": "테스트 코드 작성/수정", + "order": 223, + "default": true + }, + { + "name": "Type: Chore :package:", + "color": "D9B387", + "description": "유지보수성 작업(패키지 관리, 빌드/CI 설정 등)", + "order": 231, + "default": true + }, + { + "name": "Type: Deploy :rocket:", + "color": "BFDACD", + "description": "배포 관련 작업(스크립트 작성, 배포 환경 설정 등)", + "order": 241, + "default": true + }, + + { + "name": "Scope: FE", + "color": "4A4A4A", + "description": "프론트엔드 관련 작업", + "order": 311, + "default": true + }, + { + "name": "Scope: BE", + "color": "C0C0C0", + "description": "백엔드 관련 작업", + "order": 321, + "default": true + }, + { + "name": "Scope: Infra", + "color": "D7B75B", + "description": "인프라 관련 작업 (배포, 서버 등)", + "order": 331, + "default": true + }, + { + "name": "Scope: Github", + "color": "7E6F7B", + "description": "Github 관련 작업(Actions, Issue 등)", + "order": 341, + "default": true + } +] diff --git a/index.js b/index.js new file mode 100644 index 0000000..cf08084 --- /dev/null +++ b/index.js @@ -0,0 +1,45 @@ +#!/usr/bin/env node +const { Command } = require("commander"); +const program = new Command(); + +program.name("devkit").description("GitHub workflow automation CLI").version("0.1.0"); + +// command: 저장소 관리 +program + .command("repo") + .description("GitHub 저장소 관리") + .option("-c, --create", "(옵션) 새로운 저장소 생성") + .action(require("./commands/repo")); + +// command: 라벨 관리 +program + .command("label") + .description("GitHub 라벨 관리") + .option("-c, --create", "(옵션) 라벨 목록 생성") + .option("-r, --reset", "(옵션) 기존 라벨 제거") + .action(require("./commands/label")); + +// command: 이슈 관리 +program + .command("issue") + .description("GitHub 이슈 관리") + .option("-c, --create", "(옵션) 새로운 이슈 생성") + .action(require("./commands/issue")); + +// command: 브랜치 관리 +program + .command("branch") + .description("GitHub 브랜치 관리") + .option("-c, --create", "(옵션) 새로운 브랜치 생성") + .action(require("./commands/branch")); + +// command: 설정 관리 +program + .command("config") + .description("GitHub DevKit 설정 관리") + .option("-t, --token", "(옵션) GitHub 토큰 설정") + .option("-p, --path", "(옵션) 클론 기본 경로 설정") + .option("-s, --show", "(옵션) 현재 설정 보기") + .action(require("./commands/config")); + +program.parse(process.argv); diff --git a/libs/git.js b/libs/git.js new file mode 100644 index 0000000..53f5ddc --- /dev/null +++ b/libs/git.js @@ -0,0 +1,61 @@ +const cmd = require("../utils/system/cmd"); + +const git = { + currentOwner: () => { + try { + const url = cmd.execWithResult("git remote get-url origin").trim(); + // GitHub URL 패턴: https://github.com/owner/repo.git 또는 git@github.com:owner/repo.git + let owner; + if (url.includes("github.com/")) { + // HTTPS 형식 + owner = url.split("github.com/")[1].split("/")[0]; + } else if (url.includes("github.com:")) { + // SSH 형식 + owner = url.split("github.com:")[1].split("/")[0]; + } else { + return null; + } + return owner; + } catch (err) { + return null; + } + }, + + currentRepo: () => { + try { + const url = cmd.execWithResult("git remote get-url origin").trim(); + const repo = url + .split("/") + .pop() + .replace(/\.git$/, ""); + return repo; + } catch (err) { + return null; + } + }, + + clone: (url) => { + cmd.exec(`git clone ${url}`); + }, + + commitAll: (message) => { + cmd.exec(`git add .`); + cmd.exec(`git commit -m "${message}"`); + }, + push: (branch) => { + cmd.exec(`git push -u origin ${branch}`); + }, + + checkout: (branch) => { + cmd.exec(`git checkout ${branch}`); + }, + checkoutWithNewBranch: (branch, baseBranch) => { + cmd.exec(`git checkout -b ${branch} ${baseBranch}`); + }, + + fetch: (branch) => { + cmd.exec(`git fetch origin ${branch}`); + }, +}; + +module.exports = git; diff --git a/libs/octokit.js b/libs/octokit.js new file mode 100644 index 0000000..d54b8f4 --- /dev/null +++ b/libs/octokit.js @@ -0,0 +1,15 @@ +const { Octokit } = require("@octokit/rest"); +const env = require("../config/env"); + +const octokit = { + instance: null, + async getInstance() { + if (this.instance) return this.instance; + + await env.load(); + this.instance = new Octokit({ auth: env.GITHUB_TOKEN }); + return this.instance; + }, +}; + +module.exports = octokit; diff --git a/package.json b/package.json new file mode 100644 index 0000000..ed3faed --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "github-devkit", + "description": "Automation development kit for Github workflows", + "main": "index.js", + "bin": { + "gdk": "index.js" + }, + "scripts": {}, + "dependencies": { + "@octokit/rest": "^18.0.12", + "chalk": "^4.1.2", + "commander": "^8.3.0", + "dotenv": "^10.0.0", + "inquirer": "^8.2.4" + } +} diff --git a/utils/prompt.js b/utils/prompt.js new file mode 100644 index 0000000..c3530d9 --- /dev/null +++ b/utils/prompt.js @@ -0,0 +1,131 @@ +const inquirer = require("inquirer"); +const labels = require("../data/labels.json"); + +const prompt = { + username: async () => { + return await inquirer.prompt([ + { + type: "input", + name: "username", + message: "사용자 이름:", + validate: (input) => input.trim() !== "" || "사용자 이름을 입력해주세요.", + }, + ]); + }, + createRepo: async () => { + return await inquirer.prompt([ + { + type: "input", + name: "name", + message: "저장소 이름:", + validate: (input) => input.trim() !== "" || "저장소 이름을 입력해주세요.", + }, + { + type: "input", + name: "description", + message: "저장소 설명 (선택):", + }, + { + type: "confirm", + name: "isPrivate", + message: "저장소를 공개로 설정할까요? (기본값: true)", + default: true, + }, + ]); + }, + repo: async () => { + return await inquirer.prompt([ + { + type: "input", + name: "repo", + message: "저장소 이름:", + validate: (input) => input.trim() !== "" || "저장소 이름을 입력해주세요.", + }, + ]); + }, + selectLabels: async () => { + return await inquirer.prompt([ + { + type: "checkbox", + name: "selected", + message: "생성할 라벨을 선택하세요:", + choices: labels + .sort((a, b) => a.order - b.order) + .map((label) => ({ + name: `${label.name} (${label.description})`, + value: label, + checked: label.default === true, + })), + }, + ]); + }, + issue: async () => { + return await inquirer.prompt([ + { + type: "input", + name: "issueTitle", + message: "이슈 제목:", + validate: (input) => input.trim() !== "" || "이슈 제목을 입력해주세요.", + }, + { + type: "input", + name: "issueBody", + message: "이슈 설명 (선택):", + }, + ]); + }, + createBranch: async () => { + return await inquirer.prompt([ + // TODO: 이슈 목록 불러와 표시 + { + type: "input", + name: "issueNumber", + message: "이슈 번호:", + validate: (input) => + input.trim() !== "" && /^\d+$/.test(input) ? true : "이슈 번호는 숫자로만 입력해주세요. 예: 123", + }, + // TODO: 브랜치 구분 자동감지 + { + type: "input", + name: "prefix", + message: "브랜치 구분 (기본값: feat):", + default: "feat", + }, + // TODO: 브랜치 이름 자동감지 + { + type: "input", + name: "branchName", + message: "브랜치 이름(예: auth, user, ..):", + validate: (input) => input.trim() !== "" || "브랜치 이름을 입력해주세요.", + }, + { + type: "input", + name: "baseBranch", + message: "어떤 브랜치로부터 분기할까요? (기본값: develop):", + default: "develop", + }, + ]); + }, + githubToken: async () => { + return await inquirer.prompt([ + { + type: "input", + name: "githubToken", + message: "GitHub 토큰:", + validate: (input) => input.trim() !== "" || "토큰 문자열을 입력해주세요.", + }, + ]); + }, + githubUsername: async () => { + return await inquirer.prompt([ + { + type: "input", + name: "githubUsername", + message: "GitHub 사용자명:", + validate: (input) => input.trim() !== "" || "사용자명을 입력해주세요.", + }, + ]); + }, +}; + +module.exports = prompt; diff --git a/utils/system/cmd.js b/utils/system/cmd.js new file mode 100644 index 0000000..b531b14 --- /dev/null +++ b/utils/system/cmd.js @@ -0,0 +1,15 @@ +const { execSync, exec } = require("child_process"); + +const cmd = { + exec: (command) => { + return execSync(command, { stdio: "inherit" }); + }, + execWithResult: (command) => { + return execSync(command, { encoding: "utf-8" }); + }, + execAsync: (command) => { + return exec(command); + }, +}; + +module.exports = cmd; diff --git a/utils/system/fs.js b/utils/system/fs.js new file mode 100644 index 0000000..21da40a --- /dev/null +++ b/utils/system/fs.js @@ -0,0 +1,14 @@ +const fs = require("fs"); + +function readFile(path) { + return fs.readFileSync(path, "utf-8"); +} + +function writeFile(path, content) { + fs.writeFileSync(path, content); +} + +module.exports = { + readFile, + writeFile, +}; diff --git a/utils/system/path.js b/utils/system/path.js new file mode 100644 index 0000000..9687106 --- /dev/null +++ b/utils/system/path.js @@ -0,0 +1,21 @@ +const path = require("path"); + +const PATH_ROOT = path.resolve(__dirname, "../../"); + +function getRootPath() { + return PATH_ROOT; +} + +function resolveAbsPath(relativePath) { + return path.resolve(PATH_ROOT, relativePath); +} + +function moveTo(relativePath) { + process.chdir(resolveAbsPath(relativePath)); +} + +module.exports = { + getRootPath, + resolveAbsPath, + moveTo, +}; From ed055c1ffe86d92cd4c57804386e14c77de84642 Mon Sep 17 00:00:00 2001 From: wsk0715 Date: Thu, 5 Jun 2025 14:13:56 +0900 Subject: [PATCH 2/2] =?UTF-8?q?:memo:=20v0.1.0=20=EB=A6=B4=EB=A6=AC?= =?UTF-8?q?=EC=A6=88=20=EB=AC=B8=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 25 ++++++++ README.md | 164 ++++++++++++++++++++++++++++++++++++++++++++++++--- package.json | 34 ++++++----- 3 files changed, 200 insertions(+), 23 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..95ee102 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# 변경 사항 + +이 프로젝트의 주요 업데이트 내역을 안내합니다. + +## [0.1.0] - 2025-06-05 + +### 새로운 기능 +- **CLI 도구**: GitHub 워크플로우 자동화를 위한 명령줄 인터페이스(GitHub DevKit, `gdk`)를 제공합니다. +- **저장소 관리**: `gdk repo -c` 명령으로 GitHub 저장소를 생성하고, 로컬에 클론할 수 있습니다. +- **라벨 관리**: + - `gdk label -c`로 미리 준비된 라벨을 한 번에 생성할 수 있습니다. + - `gdk label -r`로 기존 라벨을 초기화할 수 있습니다. + - `data/labels.json`을 수정해 커스텀 라벨 목록을 생성할 수 있습니다. +- **이슈 관리**: `gdk issue -c` 명령으로 GitHub 이슈를 간편하게 생성할 수 있습니다. +- **브랜치 관리**: `gdk branch -c` 명령으로 이슈와 연결된 브랜치를 쉽게 만들 수 있습니다. +- **환경 설정**: + - `gdk config`로 대화형 설정 메뉴를 제공합니다. + - `gdk config -t`로 GitHub Token(Classic)을 등록할 수 있습니다. + - `gdk config -s`로 현재 설정을 확인할 수 있습니다. + - 토큰 자동 검증 및 사용자 정보 자동 감지 기능을 포함합니다. +- **환경 관리**: `config/env.json` 파일에 환경 설정이 자동으로 저장됩니다. +- **경로 설정**: + - `gdk config -p`로 저장소 클론 기본 경로를 설정할 수 있습니다. + - 저장소 생성 시 클론 경로를 선택할 수 있습니다. + - 기본 경로 설정으로 반복적인 경로 입력을 줄일 수 있습니다. diff --git a/README.md b/README.md index 7070186..14d9967 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,165 @@ -# github-workflow-automation +# GitHub DevKit (gdk) -- 프로젝트 과정에서 발생하는 반복적인 Github 작업을 자동화하는 유틸리티 +GitHub 워크플로우를 자동화하는 CLI 도구입니다. 저장소 생성, 라벨 관리, 이슈 생성, 브랜치 관리 등 반복적인 GitHub 작업을 명령어로 간편하게 처리할 수 있습니다. -## Getting Started +## 설치(Installation) -### Requirements +```bash +npm install -g . +``` + +## 초기 설정(Initial Setup) + +GitHub Token(Classic)을 설정해야 합니다: + +```bash +# 대화형 설정 메뉴 +gdk config + +# 또는 직접 토큰 설정 +gdk config -t +``` -- Node.js +토큰은 다음 권한이 필요합니다: +- `repo` (저장소 관리) +- `public_repo` (공개 저장소) +- `user` (사용자 정보) + +## 명령어(Commands) + +### 설정 관리(Configuration Management) + +```bash +# 대화형 설정 메뉴 +gdk config + +# GitHub 토큰 설정 +gdk config -t + +# 저장소 클론 기본 경로 설정 +gdk config -p + +# 현재 설정 조회 +gdk config -s +``` -### Installation & Run +### 저장소 관리(Repository Management) ```bash -npm install +# 새 저장소 생성 및 로컬 클론 +gdk repo -c ``` + +새 저장소를 생성하고 복제합니다. 저장소명, 설명, 공개/비공개 설정을 대화형으로 입력받습니다. + +### 라벨 관리(Label Management) + +```bash +# 미리 정의된 라벨 생성 +gdk label -c + +# 기존 라벨 제거 후 새 라벨 생성 +gdk label -r +``` + +미리 정의된 라벨 템플릿을 사용하여 일관된 라벨 시스템을 구축할 수 있습니다. 또는 `data/labels.json`을 수정하여 커스텀 라벨 목록을 생성할 수 있습니다. + +**기본 라벨 목록(Default Labels):** + +``` +상태(Status) +- Needs-Triage 🎯 - 분류 대기중 +- Hotfix 🔥 - 치명적인 버그(긴급 수정 필요) +- Bug 🐛 - 치명적이지 않은 버그 +- ASAP ⚡ - 우선순위가 높은 작업 + +진행(Progress) +- Status: In Progress ⏳ - 작업 진행 중 +- Status: Paused ⏸️ - 작업이 일시 중단됨 +- Status: Blocked 🚫 - 다른 작업/요인에 의해 막힘 +- Status: Done ✅ - 작업 완료됨 +- Status: Aborted ❌ - 작업 취소됨 + +작업 유형(Type) +- Type: Documentation 📝 - 문서 관련 작업 +- Type: UI/UX 🎨 - UI/UX 관련 작업 +- Type: Feature ✨ - 새로운 기능 추가 +- Type: Enhancement ⚡ - 기존 기능 개선 +- Type: Refactor ♻️ - 코드 리팩터링 +- Type: Fix 🐛 - 버그 수정 +- Type: Test 🧪 - 테스트 코드 작성/수정 +- Type: Chore 📦 - 유지보수성 작업 +- Type: Deploy 🚀 - 배포 관련 작업 + +작업 범위(Scope) +- Scope: FE - 프론트엔드 관련 작업 +- Scope: BE - 백엔드 관련 작업 +- Scope: Infra - 인프라 관련 작업 +- Scope: Github - Github 관련 작업 +``` + +### 이슈 관리(Issue Management) + +```bash +# 새 이슈 생성 +gdk issue -c +``` + +제목, 내용, 라벨, 담당자를 대화형으로 입력받아 새 이슈를 생성합니다. + +### 브랜치 관리(Branch Management) + +```bash +# 새 브랜치 생성 +gdk branch -c +``` + +GitHub에서 새 브랜치를 생성합니다. 베이스 브랜치와 새 브랜치명을 입력받습니다. + +## 사용 예시(Usage Examples) + +### 새 프로젝트 시작하기(Starting a New Project) + +```bash +# 1. 설정 +gdk config + +# 2. 저장소 생성 +gdk repo -c + +# 3. 라벨 시스템 설정 +gdk label -r + +# 4. 이슈 생성 +gdk issue -c + +# 5. 개발 브랜치 생성 +gdk branch -c +``` + +### 기존 프로젝트에 라벨 추가(Add Labels on Existing Project) + +```bash +# 기존 라벨 제거하고 새 라벨 시스템 적용 +gdk label -r +``` + +## 설정 파일(Configuration File) + +설정은 `config/env.json`에 저장됩니다: + +```json +{ + "GITHUB_TOKEN": "your_github_token", + "GITHUB_USERNAME": "your_username" +} +``` + +## 요구사항(Requirements) + +- Node.js 14.0 이상 +- GitHub Token(Classic) + +## 라이센스(License) + +MIT diff --git a/package.json b/package.json index ed3faed..9c0cb1e 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,18 @@ -{ - "name": "github-devkit", - "description": "Automation development kit for Github workflows", - "main": "index.js", - "bin": { - "gdk": "index.js" - }, - "scripts": {}, - "dependencies": { - "@octokit/rest": "^18.0.12", - "chalk": "^4.1.2", - "commander": "^8.3.0", - "dotenv": "^10.0.0", - "inquirer": "^8.2.4" - } -} +{ + "name": "github-devkit", + "version": "0.1.0", + "description": "Development automation kit for Github workflows", + "main": "index.js", + "bin": { + "gdk": "index.js" + }, + "scripts": {}, + "dependencies": { + "@octokit/rest": "^18.0.12", + "chalk": "^4.1.2", + "commander": "^8.3.0", + "dotenv": "^10.0.0", + "inquirer": "^8.2.4" + }, + "license": "MIT" +}