平常經常使用一些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來實現命令行工具還是比較簡單的。目前有很多比較成熟的工具庫,基本上常用的功能都能夠實現。如果有需要自己造輪子,大家可以參考本文的實現思路。
參考資料
代碼參考https://github.com/Tereflech17/musette
https://github.com/tj/commander.js/blob/HEAD/Readme_zh-CN.md
https://www.lynda.com/Node-js-tutorials/Building-Your-First-CLI-App-Node