如何開發一個屬於自己的命令行工具 創建基礎的文件目錄 功能模塊 總結 參考資料

平常經常使用一些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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章