当前位置: 首页 > news >正文

如何打造一个属于自己的命令行工具

平常经常使用一些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来实现命令行工具还是比较简单的。目前有很多比较成熟的工具库,基本上常用的功能都能够实现。如果有需要自己造轮子,大家可以参考本文的实现思路
 

相关文章:

  • git bash使用vue-cli创建项目无法切换选项
  • 线程和进程的区别是什么?
  • vue中的mixins(混入)使用场景介绍
  • webpack中,devServer.proxy跨域无效的解决办法
  • 数组里的字符串转换成数字或者把数字转换成字符串
  • 小米运动蓝牙耳机使用说明书-如果第二次切换到配对状态
  • el-input只能输入数字
  • element多个弹窗层级问题
  • 浅谈测试左移和测试右移
  • js去除字符串中的所有空格(包括前后,中间存在的所有空格)
  • js判断数组是否有重复值
  • 0-100
  • CSS文本超过两行用省略号代替(兼容所有浏览器)
  • 穿梭框+el-tree,递归实践
  • el-tree点击变disabled
  • 【vuex入门系列02】mutation接收单个参数和多个参数
  • angular2开源库收集
  • CSS盒模型深入
  • Go 语言编译器的 //go: 详解
  • iOS小技巧之UIImagePickerController实现头像选择
  • mac修复ab及siege安装
  • MySQL几个简单SQL的优化
  • MySQL-事务管理(基础)
  • 初识MongoDB分片
  • 如何用vue打造一个移动端音乐播放器
  • 阿里云ACE认证学习知识点梳理
  • 函数计算新功能-----支持C#函数
  • ​卜东波研究员:高观点下的少儿计算思维
  • #AngularJS#$sce.trustAsResourceUrl
  • #常见电池型号介绍 常见电池尺寸是多少【详解】
  • $$$$GB2312-80区位编码表$$$$
  • $.ajax,axios,fetch三种ajax请求的区别
  • (1)常见O(n^2)排序算法解析
  • (done) 两个矩阵 “相似” 是什么意思?
  • (NO.00004)iOS实现打砖块游戏(九):游戏中小球与反弹棒的碰撞
  • (Redis使用系列) Springboot 实现Redis 同数据源动态切换db 八
  • (读书笔记)Javascript高级程序设计---ECMAScript基础
  • (附源码)springboot“微印象”在线打印预约系统 毕业设计 061642
  • (附源码)springboot高校宿舍交电费系统 毕业设计031552
  • (转) Face-Resources
  • (转)ORM
  • ... fatal error LINK1120:1个无法解析的外部命令 的解决办法
  • .bat批处理(八):各种形式的变量%0、%i、%%i、var、%var%、!var!的含义和区别
  • .equal()和==的区别 怎样判断字符串为空问题: Illegal invoke-super to void nio.file.AccessDeniedException
  • .net core 微服务_.NET Core 3.0中用 Code-First 方式创建 gRPC 服务与客户端
  • .NET的数据绑定
  • .NET框架
  • .Net转Java自学之路—SpringMVC框架篇六(异常处理)
  • @ComponentScan比较
  • @RequestBody详解:用于获取请求体中的Json格式参数
  • @开发者,一文搞懂什么是 C# 计时器!
  • [ Linux ] git工具的基本使用(仓库的构建,提交)
  • [ActionScript][AS3]小小笔记
  • [BZOJ5250][九省联考2018]秘密袭击(DP)
  • [C++] Boost智能指针——boost::scoped_ptr(使用及原理分析)