美文网首页
如何开发一个属于自己的命令行工具

如何开发一个属于自己的命令行工具

作者: SCQ000 | 来源:发表于2020-07-15 09:10 被阅读0次

平常经常使用一些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,figletcommander这几个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

相关文章

网友评论

      本文标题:如何开发一个属于自己的命令行工具

      本文链接:https://www.haomeiwen.com/subject/qcjnactx.html