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

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

作者: 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