diff --git a/package.json b/package.json index ede7ce6..2df518b 100644 --- a/package.json +++ b/package.json @@ -25,18 +25,18 @@ "license": "MIT", "devDependencies": { "@antfu/eslint-config": "^0.41.0", + "@clack/prompts": "^0.7.0", "@rollup/plugin-commonjs": "^25.0.4", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.2.1", + "@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-terser": "^0.4.3", "@rollup/plugin-typescript": "^11.1.3", "@types/node": "^20.5.9", "commander": "^11.0.0", + "node-emoji": "^2.1.0", "rollup": "^3.28.1", "ts-node": "^10.9.1", "typescript": "^5.2.2" - }, - "dependencies": { - "consola": "^3.2.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eea9cc5..1aa1164 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,15 +4,13 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - consola: - specifier: ^3.2.3 - version: 3.2.3 - devDependencies: '@antfu/eslint-config': specifier: ^0.41.0 version: 0.41.0(eslint@8.48.0)(typescript@5.2.2) + '@clack/prompts': + specifier: ^0.7.0 + version: 0.7.0 '@rollup/plugin-commonjs': specifier: ^25.0.4 version: 25.0.4(rollup@3.28.1) @@ -22,6 +20,9 @@ devDependencies: '@rollup/plugin-node-resolve': specifier: ^15.2.1 version: 15.2.1(rollup@3.28.1) + '@rollup/plugin-replace': + specifier: ^5.0.2 + version: 5.0.2(rollup@3.28.1) '@rollup/plugin-terser': specifier: ^0.4.3 version: 0.4.3(rollup@3.28.1) @@ -34,6 +35,9 @@ devDependencies: commander: specifier: ^11.0.0 version: 11.0.0 + node-emoji: + specifier: ^2.1.0 + version: 2.1.0 rollup: specifier: ^3.28.1 version: 3.28.1 @@ -169,6 +173,23 @@ packages: js-tokens: 4.0.0 dev: true + /@clack/core@0.3.3: + resolution: {integrity: sha512-5ZGyb75BUBjlll6eOa1m/IZBxwk91dooBWhPSL67sWcLS0zt9SnswRL0l26TVdBhb0wnWORRxUn//uH6n4z7+A==} + dependencies: + picocolors: 1.0.0 + sisteransi: 1.0.5 + dev: true + + /@clack/prompts@0.7.0: + resolution: {integrity: sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==} + dependencies: + '@clack/core': 0.3.3 + picocolors: 1.0.0 + sisteransi: 1.0.5 + dev: true + bundledDependencies: + - is-unicode-supported + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -340,6 +361,20 @@ packages: rollup: 3.28.1 dev: true + /@rollup/plugin-replace@5.0.2(rollup@3.28.1): + resolution: {integrity: sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.4(rollup@3.28.1) + magic-string: 0.27.0 + rollup: 3.28.1 + dev: true + /@rollup/plugin-terser@0.4.3(rollup@3.28.1): resolution: {integrity: sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==} engines: {node: '>=14.0.0'} @@ -389,6 +424,11 @@ packages: rollup: 3.28.1 dev: true + /@sindresorhus/is@3.1.2: + resolution: {integrity: sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ==} + engines: {node: '>=10'} + dev: true + /@tsconfig/node10@1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} dev: true @@ -757,6 +797,11 @@ packages: supports-color: 7.2.0 dev: true + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + dev: true + /character-entities-legacy@1.1.4: resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} dev: true @@ -819,11 +864,6 @@ packages: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} dev: true - /consola@3.2.3: - resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} - engines: {node: ^14.18.0 || >=16.10.0} - dev: false - /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true @@ -928,6 +968,10 @@ packages: domhandler: 5.0.3 dev: true + /emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + dev: true + /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1779,6 +1823,15 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /node-emoji@2.1.0: + resolution: {integrity: sha512-tcsBm9C6FmPN5Wo7OjFi9lgMyJjvkAeirmjR/ax8Ttfqy4N8PoFic26uqFTIgayHPNI5FH4ltUvfh9kHzwcK9A==} + dependencies: + '@sindresorhus/is': 3.1.2 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + dev: true + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -1897,6 +1950,10 @@ packages: engines: {node: '>=8'} dev: true + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -2045,6 +2102,17 @@ packages: engines: {node: '>=8'} dev: true + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: true + + /skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + dependencies: + unicode-emoji-modifier-base: 1.0.0 + dev: true + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2230,6 +2298,11 @@ packages: hasBin: true dev: true + /unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + dev: true + /unist-util-stringify-position@2.0.3: resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} dependencies: diff --git a/rollup.config.mjs b/rollup.config.mjs index 5b0265c..c8f6111 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,8 +1,11 @@ +import process from 'node:process' import ts from '@rollup/plugin-typescript' import json from '@rollup/plugin-json' import terser from '@rollup/plugin-terser' import nodeResolve from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' +import replace from '@rollup/plugin-replace' +import packageJson from './package.json' assert { type: 'json' } export default { input: 'src/index.ts', @@ -21,5 +24,10 @@ export default { exportConditions: ['node', 'default', 'module', 'import'], browser: false, }), + replace({ + 'preventAssignment': true, + 'process.env.npm_package_version': `'${packageJson.version}'`, + }), + ], } diff --git a/src/config.json b/src/config.json index 985fe81..3e2ab27 100644 --- a/src/config.json +++ b/src/config.json @@ -4,87 +4,113 @@ "data": { "feat": { "display": "feat", - "emoji": ":sparkles:" + "emoji": ":sparkles:", + "selectable": true }, "chore": { "display": "chore", - "emoji": ":wrench:" + "emoji": ":wrench:", + "selectable": true + }, + "fix": { + "display": "fix", + "emoji": ":adhesive_bandage:", + "selectable": true }, "refactor": { "display": "refactor", - "emoji": ":hammer:" + "emoji": ":hammer:", + "selectable": true }, - "fix": { - "display": "fix", - "emoji": ":adhesive_bandage:" + "style": { + "display": "style", + "emoji": ":art:", + "selectable": true }, - "hotfix": { - "display": "hotfix", - "emoji": ":ambulance:" + "move": { + "display": "move", + "emoji": ":truck:", + "selectable": true }, "docs": { "display": "docs", - "emoji": ":books:" + "emoji": ":books:", + "selectable": true }, - "i18n": { - "display": "i18n", - "emoji": ":globe_with_meridians:" + "wip": { + "display": "wip", + "emoji": ":construction:", + "selectable": true + }, + "init": { + "display": "init", + "emoji": ":tada:", + "selectable": true + }, + "release": { + "display": "release", + "emoji": ":bookmark:", + "selectable": true + }, + "hotfix": { + "display": "hotfix", + "emoji": ":ambulance:", + "selectable": false }, "build": { "display": "build", - "emoji": ":building_construction:" + "emoji": ":package:", + "selectable": false }, - "version": { - "display": "version", - "emoji": ":bookmark:" + "i18n": { + "display": "i18n", + "emoji": ":globe_with_meridians:", + "selectable": false }, "test": { "display": "test", - "emoji": ":rotating_light:" + "emoji": ":rotating_light:", + "selectable": false }, "ci": { "display": "ci", - "emoji": ":construction_worker:" + "emoji": ":robot:", + "selectable": false }, "cd": { "display": "cd", - "emoji": ":construction_worker:" + "emoji": ":robot:", + "selectable": false }, "workflow": { "display": "workflow", - "emoji": ":construction_worker:" - }, - "wip": { - "display": "wip", - "emoji": ":construction_worker:" - }, - "init": { - "display": "init", - "emoji": ":tada:" - }, - "style": { - "display": "style", - "emoji": ":art:" + "emoji": ":robot:", + "selectable": false }, "docker": { "display": "docker", - "emoji": ":whale:" + "emoji": ":whale:", + "selectable": false }, "revert": { "display": "revert", - "emoji": ":rewind:" + "emoji": ":rewind:", + "selectable": false }, "config": { "display": "config", - "emoji": ":wrench:" + "emoji": ":wrench:", + "selectable": false }, - "release": { - "display": "release", - "emoji": ":bookmark:" + "version": { + "display": "version", + "emoji": ":bookmark:", + "selectable": false }, "tag": { "display": "tag", - "emoji": ":bookmark:" + "emoji": ":bookmark:", + "selectable": false } } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ea6faa4..e3059b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,9 +4,27 @@ import process from 'node:process' import { exec } from 'node:child_process' import os from 'node:os' import { program } from 'commander' -import { consola } from 'consola' +import { confirm, intro, isCancel, log, note, select, spinner, text } from '@clack/prompts' +import { emojify } from 'node-emoji' import defaultConfig from './config.json' assert { type: 'json' } +const version = process.env.npm_package_version + +let config = defaultConfig +const configPath = path.join(os.homedir(), '.config/gitcm/config.json') +function getLatestVersion() { + return new Promise((resolve, reject) => { + exec('npm view @gitcm/cli version', (err, stdout) => { + if (err) { + reject(err) + return + } + resolve(stdout.trim()) + }) + }) +} + +const latestVersionPromise = getLatestVersion() program .name('git-commit') .usage('[type] [scope] [body]') @@ -19,19 +37,6 @@ program program.parse() -let config = defaultConfig - -const configPath = path.join(os.homedir(), '~/.config/gitcm/config.json') -if (!fs.existsSync(configPath)) { - const dir = path.dirname(configPath) - fs.mkdirSync(dir, { recursive: true }) - consola.info(`Config file not found. Create config file in ${configPath}`) - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)) -} -else { - config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) -} - const options = program.opts() config.showIcon = !options.noIcon config.verbose = options.verbose @@ -54,6 +59,28 @@ if (args.length > 0) { } const typeList = Object.keys(config.data) +function getDisplay(type: string) { + if (!isType(type)) + return type + return config.data[type].display +} + +function isSelectable(type: string) { + if (!isType(type)) + return false + return config.data[type].selectable +} + +function getEmoji(type: string) { + if (!isType(type)) + return '' + switch (config.data[type].emoji) { + case ':adhesive_bandage:': + return '🩹' + } + return emojify(config.data[type].emoji) +} + function isType(key: string): key is keyof typeof config.data { return key in config.data } @@ -65,74 +92,131 @@ function getCMD({ type, scope, body, icon }: { type: string; body: string; scope } async function waitPrompt() { + intro(`@gitcm/cli - v${version}`) + + if (!fs.existsSync(configPath)) { + const dir = path.dirname(configPath) + fs.mkdirSync(dir, { recursive: true }) + log.info(`Config file not found. Create config file in ${configPath}`) + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)) + } + else { + log.info(`Config file found in ${configPath}`) + config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) + } + + if (type !== '' && !isType(type)) { + log.error(`Invalid commit type: ${type}. ` + `Commit type must be one of ${typeList.join(', ')}`) + process.exit(1) + } if (!options.yes) await fillEmpty() - - await confirm() + await waitConfirm() + await checkNewVersion() + log.info('Done') } -async function confirm() { +async function waitConfirm() { if (!isType(type)) { - consola.error(`Invalid commit type: ${type}`, `Commit type must be one of ${typeList.join(', ')}`) + log.error(`Invalid commit type: ${type}. ` + `Commit type must be one of ${typeList.join(', ')}`) process.exit(1) } const cmd = getCMD({ type: config.data[type].display, scope, body, icon: config.showIcon ? config.data[type].emoji : '' }) - const answer = options.yes ? true : await consola.prompt(`Are you sure to execute this command? (${cmd})`, { type: 'confirm', initial: true }) - if (typeof answer === 'symbol') { - consola.info('Canceled') + note(cmd, 'Your commit command is') + const answer = options.yes ? true : await confirm({ message: 'Are you sure to execute this command?' }) + if (isCancel(answer)) { + log.info('Canceled by user') process.exit(0) } if (answer) { if (options.dryRun) { - consola.info(`Dry run: ${cmd}`) + log.info(`Dry run: ${cmd}`) } else { - exec(cmd, (err, stdout) => { - if (err) { - consola.error(stdout) - return - } - consola.success(stdout) - }) + try { + await new Promise((resolve, reject) => { + exec(cmd, (err, stdout) => { + if (err) { + log.error(stdout) + reject(err) + return + } + log.success(stdout) + resolve() + }) + }) + } + catch (e) { + log.error(`${e}`) + process.exit(1) + } } } else { - consola.info('Canceled') + log.info('Canceled by user') } } async function fillEmpty() { if (type === '') { - type = await consola.prompt('What is the commit type? (required)', { type: 'select', options: typeList }) - - // check type is clack:cancel - if (typeof type === 'symbol') { - consola.info('Canceled') + const typeRes = await select<{ value: string; label: string }[], string>({ + message: 'What is the commit type? (required)', + options: typeList.filter(isSelectable).map((d) => { + return { + value: d, + label: `${getEmoji(d)} ${getDisplay(d)}`, + } + }), + }) + if (isCancel(typeRes)) { + log.info('Canceled by user') process.exit(0) } + type = typeRes if (!isType(type)) { - consola.error(`Invalid commit type: ${type}`, `Commit type must be one of ${typeList.join(', ')}`) + log.error(`Invalid commit type: ${type}. ` + `Commit type must be one of ${typeList.join(', ')}`) process.exit(1) } } - if (scope === '' && args.length !== 2) - scope = await consola.prompt('What is the commit scope? (optional)', { type: 'text', default: '', placeholder: 'Enter commit scope' }) - // check type is clack:cancel - if (typeof scope === 'symbol') { - consola.info('Canceled') - process.exit(0) + if (scope === '' && args.length !== 2) { + const res = await text({ + message: 'What is the commit scope? (optional)', + placeholder: 'No scope', + defaultValue: '', + }) + if (isCancel(res)) { + log.info('Canceled by user') + process.exit(0) + } + scope = res.trim() } - if (body === '') - body = await consola.prompt('What is the commit for? (required)', { type: 'text', default: '', placeholder: 'Enter commit body' }) - if (typeof body === 'symbol') { - consola.info('Canceled') - process.exit(0) - } if (body === '') { - consola.error('Commit body must not be empty') - process.exit(1) + const res = await text({ + message: 'What is the commit body? (required)', + placeholder: 'Enter commit body', + defaultValue: '', + validate: (value) => { + if (value.trim() === '') + return 'Commit body cannot be empty' + }, + }) + if (isCancel(res)) { + log.info('Canceled by user') + process.exit(0) + } + body = res.trim() } } + +async function checkNewVersion() { + const s = spinner() + s.start('Checking new version') + const latestVersion = await latestVersionPromise + if (latestVersion === version) + note(`New version v${latestVersion} is available. Run "npm i -g @gitcm/cli" to update`) + s.stop() +} + waitPrompt()