平常经常使用一些npm cli工具,你是否好奇这些工具又是怎么开发出来的呢?接下来这篇文章,就会介绍如何利用Node.js开发一个属于你自己的命令行工具。
创建基础的文件目录
首先,我们需要先创建一个基本的项目结构:
mkdir git-repo-cli
cd git-repo-cli
npm init #初始化项目
接着我们创建所需的文件:
touch index.js
mkdir lib
cd lib
touch files.js
touch github_credentials.js
touch inquirer.js
touch create_a_repo.js
接着我们先来写一个简单的入口程序,在index.js
文件中代码如下:
const chalk = require('chalk');
const clear = require('clear');
const figlet = require('figlet');
const commander = require('commander');
commander
.command('init')
.description('Hello world')
.action(() => {
clear();
console.log(chalk.magenta(figlet.textSync('Git Repo Cli', {
hosrizontalLayout: 'full'
})));
});
commander.parse(process.argv);
if (!commander.args.length) {
commander.help();
}
上面的代码中引用了chalk
,clear
,figlet
和commander
这几个npm库,,其中chalk
负责命令行不同颜色文本的显示,便于用户使用。clear
库负责清空命令行界面,figlet
可以在命令行中以ASCII ART形式显示文本。最后commander
库就是用来实现命令行接口最主要的库。
写完代码后,我们可以在命令行中输入如下代码启动:
node index.js init
接着我们就可以看到这样的结果:
功能模块
文件模块
把基本的架子搭起来后,我们就可以开始写功能模块的代码了。
在lib/files.js
文件中,主要要实现以下两个功能::
- 获取当前文件目录名
- 检查该路径是否已经是个git仓库
const fs = require('fs');
const path = require('path');
module.exports = {
getCurrentDirectoryBase: () => path.basename(process.cwd()),
directoryExists: (filePath) => {
try {
return fs.statSync(filePath).isDirectory();
} catch (err) {
return false;
}
},
isGitRepository: () => {
if (files.directoryExists('.git')) {
console.log(chalk.red("Sorry! Can't create a new git repo because this directory has been existed"))
process.exit();
}
}
};
询问模块
在用户在执行命令行工具的时候,需要收集一些变量信息,因此,可以我们需要利用inquirer这个npm库来实现“询问模块”。
const inquirer = require('inquirer');
module.exports = {
askGithubCredentials: () => {
const questions = [
{
name: "username",
type: 'input',
message: 'Enter your Github username or e-mail address:',
validate: function(value) {
if (value.length) {
return true;
} else {
return 'Please enter your Github username:'
}
}
},
{
name: "password",
type: 'password',
message: 'Enter your password:',
validate: function(value) {
if (value.length) {
return true;
} else {
return 'Please enter your Github username:'
}
}
}
];
return inquirer.prompt(questions);
}
}
github认证
为了实现和github的接口通信,需要获得token认证信息。因此,在lib/github_credentials.js
文件中,我们获得token,并借助configstore
库写入package.json
文件中。
const Configstore = require('configstore');
const pkg = require('../package.json')
const octokit = require('@octokit/rest')();
const _ = require('lodash');
const inquirer = require("./inquirer");
const conf = new Configstore(pkg.name);
module.exports = {
getInstance: () => {
return octokit;
},
githubAuth: (token) => {
octokit.authenticate({
type: 'oauth',
token: token
});
},
getStoredGithubToken: () => {
return conf.get('github_credentials.token');
},
setGithubCrendeitals: async () => {
const credentials = await inquirer.askGithubCredentials();
octokit.authenticate(
_.extend({
type: 'basic'
}, credentials)
)
},
registerNewToken: async () => {
// 该方法可能会被弃用,可以手动在github设置页面设置新的token
try {
const response = await octokit.oauthAuthorizations.createAuthorization({
scope: ['user', 'public_repo', 'repo', 'repo:status'],
note: 'git-repo-cli: register new token'
});
const token = response.data.token;
if (token) {
conf.set('github_credentials.token', token);
return token;
} else {
throw new Error('Missing Token', 'Can not retrive token')
}
} catch(error) {
throw error;
}
}
}
其中@octokit/rest是node端与github通信主要的库。
接下来就可以写我们的接口了:
// index.js
const github = require('./lib/gitub_credentials');
commander.
command('check-token')
.description('Check user Github credentials')
.action(async () => {
let token = github.getStoredGithubToken();
if (!token) {
await github.setGithubCredentials();
token = await github.registryNewToken();
}
console.log(token);
});
最后,在命令行中输入如下命令:
node index.js check-token
它会先会在configstore
的默认文件夹下~/.config
寻找token, 如果没有发现的话,就会提示用户输入用户名和密码后新建一场新的token。
有了token后,就可以执行github的很多的操作,我们以新建仓库为例:
首先,先在inquirer.js
中新建askRepositoryDetails
用来获取相关的repo
信息:
askRepositoryDetails: () => {
const args = require('minimist')(process.argv.slice(2));
const questions = [
{
type: 'input',
name: 'name',
message: 'Please enter a name for your repository:',
default: args._[1] || files.getCurrentDirectoryBase(),
validate: function(value) {
if (value.length) {
return true;
} else {
return 'Please enter a unique name for the repository.'
}
}
},
{
type: 'input',
name: 'description',
default: args._[2] || null,
message: 'Now enter description:'
},
{
type: 'input',
name: 'visiblity',
message: 'Please choose repo type',
choices: ['public', 'private'],
default: 'public'
}
];
return inquirer.prompt(questions);
},
askIgnoreFiles: (filelist) => {
const questions = [{
type: 'checkbox',
choices: filelist,
message: 'Please choose ignore files'
}];
return inquirer.prompt(questions);
}
接着,实现对应的新建仓库、新建.gitignore
文件等操作:
// create_a_repo.js
const _ = require('lodash');
const fs = require('fs');
const git = require('simple-git')();
const inquirer = require('./inquirer');
const gh = require('./github_credentials');
module.exports = {
createRemoteRepository: async () => {
const github = gh.getInstance(); // 获取octokit实例
const answers = await inquirer.askRepositoryDetails();
const data = {
name: answers.name,
descriptions: answers.description,
private: (answers.visibility === 'private')
};
try {
// 利用octokit 来新建仓库
const response = await github.repos.createForAuthenticatedUser(data);
return response.data.ssh_url;
} catch (error) {
throw error;
}
},
createGitIgnore: async () => {
const filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore');
if (filelist.length) {
const answers = await inquirer.askIgnoreFiles(filelist);
if (answers.ignore.length) {
fs.writeFileSync('.gitignore', answers.ignore.join('\n'));
} else {
touch('.gitnore');
}
} else {
touch('.gitignore');
}
},
setupRepo: async (url) => {
try {
await git.
init()
.add('.gitignore')
.add('./*')
.commit('Initial commit')
.addRemote('origin', url)
.push('origin', 'master')
return true;
} catch (err) {
throw err;
}
}
}
最后,在index.js
文件中新建一个create-repo
的命令,执行整个流程。
// index.js
commander
.command('create-repo')
.description('create a new repo')
.action(async () => {
try {
const token = await github.getStoredGithubToken();
github.githubAuth(token);
const url = await repo.createRemoteRepository();
await repo.createGitIgnore();
const complete = await repo.setupRepository(url);
if (complete) {
console.log(chalk.green('All done!'));
}
} catch (error) {
if (error) {
switch (error.status) {
case 401:
console.log('xxx');
break;
}
}
}
})
写完代码后,在命令行中执行如下命令即可:
node index.js create-repo
总结
总的来说,利用node.js来实现命令行工具还是比较简单的。目前有很多比较成熟的工具库,基本上常用的功能都能够实现。如果有需要自己造轮子,大家可以参考本文的实现思路