一、需求背景
在看一些包源码的时候,想了解一下作者实现这个工具的思路,工程化是如何实现的,演变到如今这个庞大的库都经历了什么。会去思考一个问题,为啥别人能写得这么好,能够有这么牛的技术深度和广度,但面对经过无数次迭代的库来说,想要阅读下源码实在太困难了,所以想着回到最初的那个版本,一步一步的看下来,这样才能够更好的理解作者的思路,也能够更好的学习到别人的技术。
当然,这个需求十分简单,可能会说用source-tree或者gitlen的图形化界面查看一下不就好了吗?确实,但命令行的形式能让我快速的切换,配合iterm2的历史记录,选择下方向键和回车确认就好了,不用冗长的时间线上去找commit,切换好分支之后,倒是可以配合对应的gitlen图形化界面更直观的查看变更文件。主要是想要利用copilot快速的编写一个node工具,体会AI辅助写代码的便捷性,找到对应的hash值用git switch切换也行。
本次需要支持的命令使用形式如下:
- -b代表指定分支基准,代表了branch
- -d代表了分支方向,指定向前还是向后查看或者是回到第一个commit
- 由于要支持多命令格式,以便未来扩展,view子命令代表查看,back子命令代表回到被查看分支。
shell
$ rh view -b main -d first
$ rh back
采用的库选择了zx
和sade
1.1 zx介绍(链接)
- 简洁易用:它提供了一种清晰而简洁的语法,使得编写和执行命令行操作变得容易。使用zx,您可以直接在脚本中编写和执行命令,而无需在Shell中切换。
- 跨平台:可以在主流操作系统上运行(如Windows、macOS、Linux等),保证了脚本在不同平台上的兼容性。
- 内置功能:提供了许多常用的功能和工具,如重试、CLI进度条、文件操作、路径操作等,使得在脚本中完成常见的任务更加方便。
- 无需导入包:内置了常用的Node.js内置模块和第三方包,您可以在脚本中直接使用它们,无需导入或安装额外的包,比如
import 'zx/globals'
将全局导入,或者可以直接引入具体的包,避免eslint错误import { echo } from 'zx'
。
1.2 sade介绍(链接)
sade是一个用于构建命令行工具的轻量级npm包,相对于市面上的其他命令行工具,具有以下几个优势:
- 轻量级:sade的设计目标是保持代码简洁,最小化依赖和内存占用。底层依赖
mri
进行命令行解析,使得构建轻量级的命令行工具变得更加容易。 - 功能强大:可以轻松地定义命令行选项、处理命令行参数、注释文档生成等等。此外,它还提供了许多有用的API和钩子,使得自定义命令行工具变得更加灵活。
1.3 平台介绍
灵犀百通 是一个免费的GPT对话文本生成网站,利用的是GPT3.5,如果你有钱的话,可以直接买ChatGPT4.0来体验更好的效果。 Vscode Copilot是一个辅助代码生成的vscode插件,联系上下文给出最佳的代码。
二、实施步骤
2.1 编写命令行解析骨架
在AI对话窗口给出想要的步骤,如果GPT不了解你使用的npm库,可以把整个npm包的README.md说明喂给它(暂时还不知道怎么喂链接给它,效果很不好,有人知道的话底下评论)
然后向它提供一些功能编写思路,就能产生大概得命令行框架模板了:
定义好命令行工具的入口:
typescript
#!/usr/bin/env zx
import sade from 'sade';
import { $, cd } from 'zx';
const prog = sade('rh');
// 定义view子命令
prog
.command('view')
.option('-b, --branch', 'Specify the branch')
.option('-d, --direction', 'Specify the direction')
.action(({ branch, direction }) => {
console.log(`Viewing branch: ${branch}`);
console.log(`Direction: ${direction}`);
// 在这里编写具体的逻辑
});
// 定义back子命令
prog
.command('back')
.action(() => {
console.log('Going back to the previous branch');
// 在这里编写具体的逻辑
});
// 解析命令行参数并执行对应的命令
prog.parse(process.argv);
在上述示例中,我们首先导入了需要使用的包:sade和zx。然后创建了一个命令行入口对象prog
,使用sade
初始化,传入命令行的名称rh
。 接下来,我们使用prog.command
方法分别定义了view
和back
两个子命令。
view
子命令使用.option
方法定义了两个选项-b, --branch
和-d, --direction
,分别用于指定分支和方向。back
子命令没有定义选项。 在.action
函数中,我们可以获取到子命令定义的选项的值,并根据需要编写具体的逻辑。 最后,使用prog.parse
解析命令行参数,并执行对应的命令。 然后在对应的.action
函数中编写逻辑来处理具体的查看和回到分支的操作。
2.2 编写本地分支校验逻辑
利用copilot编写校验本地分支,直接在函数的行注释中给出提示语,等待函数的生成按下Tab
。
这个函数的作用是检查是否存在指定的分支,并输出当前要查看源代码的分支。函数首先通过执行命令git branch获取所有的分支信息,并将结果进行处理。然后,它会检查。当查找到的分支列表中不包含目标分支时,函数会输出一条错误信息,并调用process.exit(1)来终止程序的执行。这表示程序将以退出码1退出。如果目标分支存在于查找到的分支列表中,函数将输出一条信息,指示当前要查看源代码的分支是哪个。
这时候看到copilot给出了完美的内容,不行就疯狂喂它,多写点注释。

2.3 编写切换分支函数
同样,需要你给出实现思路的注释语句,没有实现思路就去问问GPT,这里我想了想,给出的是找到所有的commit,如果direction指定为first,则回到第一个commit,next就切换到目标分支的下一个commit,pre就切换到目标分支的上一个commit
,等待代码生成按住Tab
。
这里它会逐行生成,最终生成的效果如下:
当然,你还可以对这个函数进行完善,比如说,校验本地是否有git仓库、执行next和pre的时候,遇到第一条和最后一条命令的处理方式等。
2.5 实现back子命令,回到被查看的分支
这个函数实现的功能是处理一个回退命令。具体步骤如下,当然也是copilot写的,我们只是给与实现思路:
- 使用 $ 函数执行命令 git reflog,获取 Git 的引用日志。
- 通过 stdout 属性获取命令输出结果,并使用 trim() 方法去除首尾空格。
- 使用 split('\n') 方法将输出结果按行分割,得到一个数组。
- 使用 find() 方法遍历数组,找到包含 'checkout' 的行,即最近一次执行的 checkout 操作。
- 如果没有找到最近一次的 checkout 操作,使用 echo 模板字符串输出提示信息,并使用 process.exit(1) 终止程序执行。
- 如果找到了最近一次的 checkout 操作,使用 split(' ') 方法将行按空格分割,得到一个数组。
- 通过索引为5的元素获取最近一次 checkout 操作的提交哈希值。
- 使用 $ 函数执行 git checkout 命令,将代码回退到最近一次 checkout 的提交。
- 使用 chalk 库将提示信息的部分文字着色,并使用 echo 模板字符串输出成功回退的提示信息
typescript
/**
* Handles the back command by finding the most recent checkout operation in the git reflog and checking out to the commit of the branch being viewed.
* @returns Promise<void>
*/
async function handleBackCommand() {
const lastCheckout = (await $`git reflog`).stdout.trim().split('\n').find(line => line.includes('checkout'))
if (!lastCheckout) {
echo`没有找到最近一次checkout的操作`
process.exit(1)
}
const lastCheckoutCommit = lastCheckout.split(' ')[5]
await $`git checkout ${lastCheckoutCommit}`
echo`${chalk.green(`回到 ${chalk.blue(lastCheckoutCommit)} 成功`)}`
}
2.4 为函数添加jsDoc注释
对于copilot来说,选中要添加注释的函数,按住command + i
可以快速唤起对话,然后输入/
按空格选中/doc
,等待注释的生成,点击Accept
就可以了。这都是一些快捷snippet
,背后都为你写好对应的GPT提示词了。

三、发布npm包
在发布npm包时,需要注意定义以下字段来执行bin命令:
"bin"字段:该字段用于定义要在命令行中执行的主要脚本文件的路径。在package.json中,"bin"字段的值为"rh": "./bin/rh.mjs"
,它指定了要执行的命令rh所对应的脚本文件路径为./bin/rh.mjs。
"files"字段:该字段用于指定发布包时要包含在包中的文件和目录列表。它指定了需要包含在发布的npm包中的文件和目录。"files"字段的值为["bin", "dist"],表示发布的包将包含bin目录和dist目录下的文件。
"main"字段:该字段用于指定在引入该包时,会被默认加载的入口文件的路径。在上述示例的package.json中,"main"字段的值为"./dist/rh.js",表示引入该包时,会默认加载./dist/rh.js文件。
"types"字段:该字段用于指定该包的类型声明文件的路径。在上述示例的package.json中,"types"字段的值为"./dist/rh.d.ts",表示指定了类型声明文件的路径为./dist/rh.d.ts。
"exports"字段:该字段用于指定包的模块导出方式。它允许您在不同的环境中使用不同的导入方式。在上述示例的package.json中,"exports"字段的值为:
json
"exports": {
".": {
"import": "./dist/rh.js",
"require": "./dist/rh.js",
"default": "./dist/rh.js"
}
}
typescript
import { main } from './factory'
export * from './type'
export default main
然后定义了bin脚本进行打包:
typescript
#!/usr/bin/env node
import main from '../src/index'
main()
json
{
"build": "tsup bin/rh.ts --format esm --clean --dts",
"stub": "tsup bin/rh.ts --format esm",
"dev": "tsup bin/rh.ts --format esm --watch"
}
切换到npm源,使用npm publish
进行npm包的发布。 实现的效果:

最后,可以试着使用pnpm i read-helper -g
来体验一下哦!欢迎一键三连!👏🏻