[譯]手把手教你用Node.js創(chuàng)建CLI

Node.js除了可以編寫“傳統(tǒng)“的Web應(yīng)用外沐兰,還有其他更廣泛的用途哆档。微服務(wù)、REST API住闯、工具瓜浸、物聯(lián)網(wǎng)澳淑,甚至桌面應(yīng)用,它能滿足你的任何開發(fā)需求插佛。

本文要做的事情就是利用Node.js來構(gòu)建命令行工具CLI杠巡。我們先來看一些用于創(chuàng)建命令行的第三方npm包,然后雇寇,從零開始構(gòu)建命令行工具氢拥。

我們將要實現(xiàn)一個命令行工具,它的作用是初始化Git倉庫锨侯。當(dāng)然嫩海,它不僅僅是在后臺運行git init,他還會做一些別的事情囚痴。我們可以通過它來初始化Git倉庫叁怪,并且允許用戶通過交互的方式創(chuàng)建.gitignore文件,最終執(zhí)行提交并推送代碼到遠(yuǎn)端倉庫深滚。

與以往一樣奕谭,大家可以在GitHub(https://github.com/sssssssh/ginit)上找到本教程隨附的代碼。

一痴荐、為什么用Node.js來構(gòu)建命令行工具

在深入研究之前血柳,我們有必要了解一下為什么我們選擇Node.js來構(gòu)建命令行工具。

最明顯的好處是生兆,如果你在閱讀本文那么大概率是因為你對JavaScript已經(jīng)很了解难捌。

另一個關(guān)鍵優(yōu)勢是,使用Node.js的生態(tài)意味著你可以利用成千上萬種實現(xiàn)各種目的的npm包皂贩。其中有很多是為了構(gòu)建強大的命令行工具而生的栖榨。

最后昆汹,我們可以通過npm管理依賴明刷,不需要擔(dān)心特定系統(tǒng)的包管理工具帶來的兼容問題,例如apt满粗、yum辈末、homebrew

二映皆、創(chuàng)建一個命令行工具: ginit

image

通過這個教程挤聘,我們將構(gòu)建一個叫ginit的命令行工具。它實現(xiàn)了git init捅彻,但又不僅僅只有這個功能组去。

你可能想知道它到底是干啥用的。

眾所周知步淹,git init會在當(dāng)前文件夾初始化git倉庫从隆。但是诚撵,通常這是將新項目或者已有項目關(guān)聯(lián)到Git上的眾多重復(fù)步驟中的一步。例如键闺,作為一個經(jīng)典的工作流程中的一部分寿烟,你可能會:

  1. 通過git init初始化本地倉庫
  2. 創(chuàng)建遠(yuǎn)程倉庫,這一步通常需要通過瀏覽器來完成
  3. 添加到遠(yuǎn)端
  4. 創(chuàng)建.gitignore文件
  5. 添加你自己的項目文件
  6. 提交初始項目文件
  7. 推送到遠(yuǎn)程倉庫

通常會涉及到更多操作辛燥,但是筛武,出于教學(xué)目的,在本教程中我們僅僅實現(xiàn)上面的步驟挎塌。這些步驟都是重復(fù)的徘六,我們通過命令行工具來實現(xiàn)豈不是比粘貼復(fù)制git倉庫的鏈接更好呢?

因此勃蜘,ginit要做的就是在當(dāng)前文件夾中創(chuàng)建Git倉庫硕噩,創(chuàng)建一個遠(yuǎn)程倉庫(這里我們用git),然后將它添加為遠(yuǎn)程倉庫缭贡,然后炉擅,它將提供一個簡單的交互式向?qū)韯?chuàng)建.gitignore,添加文件并將其推送到遠(yuǎn)端阳惹。他可能不會減少你的時間谍失,但是,會減少一些你的重復(fù)勞動莹汤。

基于這一點讓我們開始吧快鱼!

三、項目依賴

可以肯定的一件事:就外觀而言纲岭,控制臺永遠(yuǎn)不會具有圖形用戶界面的復(fù)雜度抹竹。不過,這并不意味著他必須是丑陋的單色文本止潮。你可能會驚訝于在保持功能正常的情況下腔丧,命令行工具也可以做的很好看激捏。我們找到了幾個增強界面展示的庫:chalk用于在終端中輸出彩色的文字谅辣;clui用于添加一些UI組件漾橙。還有好玩的,我們會利用figlet創(chuàng)建一個基于ASCII的炫酷橫幅燃乍,并且利用clear來清空控制臺唆樊。

在輸入和輸出方面,Node.js底層的Readline模塊用于提示用戶輸入綽綽有余刻蟹。但是逗旁,我們將利用一個第三方庫Inquirer,它提供了更多復(fù)雜的功能舆瘪。除了實現(xiàn)詢問用戶的功能外片效,它在控制臺中還提供了單選框和復(fù)選框的功能仓洼。

我們還會使用minimist來解析命令行中輸入的參數(shù)。

這是我們在開發(fā)命令行工具中使用到的完整的npm包列表:

  • chalk: 讓我們的輸出變得有色彩堤舒;
  • clear: 清空終端屏幕色建;
  • clui: 繪制命令行中的表格、儀表盤舌缤、加載指示器等箕戳;
  • figlet: 生成基于ASCII的藝術(shù)字;
  • inquirer: 創(chuàng)建交互式的命令行界面国撵;
  • minimist: 解析命令行參數(shù)陵吸;
  • configstore: 輕松的加載和保存配置信息;

另外介牙,我們還會使用下面的包:

  • @octokit/rest: 基于Node.js的Github REST API工具壮虫;
  • @octokit/auth-basic: Github身份驗證策略的一種實現(xiàn);
  • lodash: JavaScript 工具庫环础;
  • simple-git: 在Node.js中執(zhí)行Git命令的工具囚似;
  • touch: 實現(xiàn)Unix touch命令的工具;

四线得、開始你的表演

盡管我們是從頭開始創(chuàng)建這個命令行工具饶唤,但是不要忘記你也可以從本文附帶的GitHub倉庫(https://github.com/sssssssh/ginit)中拷貝一份代碼。

為這個項目創(chuàng)建一個新的目錄贯钩,當(dāng)然募狂,你可以給他起別的名字,不必一定叫他ginit

mkdir ginit
cd ginit

創(chuàng)建一個新的package.json文件:

npm init -y

最終將會生成一個這樣的package.json文件

{
    "name": "ginit",
    "version": "1.0.0",
    "description": "'git init' on steroids",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [
        "Git",
        "CLI"
    ],
    "license": "ISC"
}

現(xiàn)在開始安裝項目依賴:

npm install chalk clear clui figlet inquirer minimist configstore @octokit/rest @octokit/auth-basic lodash simple-git touch

在項目中創(chuàng)建一個index.js文件角雷,加上如下代碼:

// index.js

const chalk = require('chalk');
const clear = require('clear');
const figlet = require('figlet');

五祸穷、增加一些有用的方法

在目錄中創(chuàng)建一個lib目錄,并將我們的代碼分為以下模塊:

  • file.js:基礎(chǔ)的文件管理
  • inquirer.js:處理命令行中的用戶交互勺三;
  • github.js:管用戶的git token;
  • repo.js:Git倉庫管理雷滚;

讓我們開始寫lib/file.js中的代碼。我們需要做以下事情:

  1. 獲取當(dāng)前目錄(作為當(dāng)前倉庫的默認(rèn)名稱)檩咱;
  2. 檢查目錄是否存在(通過檢查.git目錄是否存在揭措,來判斷當(dāng)前目錄是否存在git倉庫)胯舷;

這聽起來很簡單刻蚯,但是,有幾個問題需要考慮桑嘶。

首先炊汹,你可能會想到用fs模塊的realpathSync方法來獲取當(dāng)前目錄:

path.basename(path.dirname(fs.realpathSync(__filename)));

當(dāng)我們從命令行所在文件的根目錄下調(diào)用時,這個方法沒啥問題逃顶。但是讨便,我們的命令行工具可以在任何目錄下調(diào)用充甚。這意味著我們需要獲得的是當(dāng)前工作目錄的名稱,而不是命令行代碼所在目錄的名稱霸褒。所以伴找,你最好使用process.cwd()

path.basename(process.cwd());

第二,檢查文件是否存在的最佳方法一直在變化废菱。目前最好的方法是使用existsSync技矮,如果文件存在他會返回true,否則返回false殊轴。

結(jié)合上面所說的衰倦,讓我們在lib/files.js中添加如下代碼:

// files.js

const fs = require('fs');
const path = require('path');

module.exports = {
    // 獲取目錄名稱
    getCurrentDirectoryBase: () => {
        return path.basename(process.cwd());
    },

    // 判斷目錄是否存在
    directoryExists: (filePath) => {
        return fs.existsSync(filePath);
    },
};

index.js中,添加下面的代碼:

// index.js

const files = require('./lib/files');

有了這個旁理,我們就可以動手開發(fā)我們的命令行工具了樊零。

六、初始化命令行工具

現(xiàn)在讓我們來實現(xiàn)命令行工具的啟動階段孽文。

為了展示我們安裝的增強控制臺輸出的模塊驻襟,我們先清空屏幕,再展示一個banner芋哭,在index.js中添加如下代碼:

// index.js

// 清除命令行
clear();

// 輸出Logo
console.log(chalk.yellow(figlet.textSync('Ginit', { horizontalLayout: 'full' })));

你可以通過運行node index.js來執(zhí)行它塑悼,輸出效果如下:

image

接下來,讓我們進(jìn)行一個簡單的檢查楷掉,以確保當(dāng)前目錄不存在git倉庫厢蒜。這很簡單,只需要利用我們創(chuàng)建的方法來檢查.git方法是否存在即可烹植,在index.js中添加如下代碼:

// 判斷是否存在.git文件
if (files.directoryExists('.git')) {
    console.log(chalk.red('已經(jīng)存在一個本地倉庫!'));
    process.exit();
}

七斑鸦、提示用戶輸入

接下來,我們需要創(chuàng)建一個函數(shù)來引導(dǎo)用戶輸入他們的GitHub賬號和密碼草雕。

我們可以使用Inquirer來實現(xiàn)巷屿,它提供了很多種類型的提示方法。這些方法有些類似于HTML中的控件墩虹。為了收集用戶的GitHub賬號和密碼嘱巾,我們需要使用到input和password類型的控件。

首先诫钓,在lib/inquirer.js中添加如下代碼:

// inquirer.js

const inquirer = require('inquirer');
const files = require('./files');

module.exports = {
    // 詢問git賬號信息
    askGithubCredentials: () => {
        const questions = [
            {
                name: 'username',
                type: 'input',
                message: '請輸入你的git賬號或郵箱地址:',
                validate: function (value) {
                    if (value.length) {
                        return true;
                    } else {
                        return '請輸入你的git賬號或郵箱地址.';
                    }
                },
            },
            {
                name: 'password',
                type: 'password',
                message: '請輸入你的密碼:',
                validate: function (value) {
                    if (value.length) {
                        return true;
                    } else {
                        return '請輸入你的密碼.';
                    }
                },
            },
        ];
        return inquirer.prompt(questions);
    }
};

如你所見旬昭,通過inquirer.prompt()向用戶詢問一系列問題,我們將這些問題以數(shù)組的形式傳遞給函數(shù)prompt菌湃。每一問題都由一個對象構(gòu)成问拘,其中,name表示該字段的名稱,type表示我們要使用控件類型骤坐,message是我們要展示給用戶的話绪杏,validate是校驗用戶輸入字段的函數(shù)。

inquirer.prompt()將會返回一個Promise對象纽绍,如果校驗通過蕾久,我們將會得到一個擁有namepassword2個屬性的對象。

將如下代碼添加在index.js

// index.js

const inquirer  = require('./lib/inquirer');

const run = async () => {
    const credentials = await inquirer.askGithubCredentials();
    console.log(credentials);
};

run();

運行node index.js結(jié)果如下:

image

提示:當(dāng)你完成測試后拌夏,不要忘了把const inquirer = require('./lib/inquirer');index.js中刪除腔彰,因為我們不需要它。

八辖佣、處理GitHub授權(quán)

下一步是創(chuàng)建一個函數(shù)霹抛,用于獲取GitHub APIOAuth TOKEN。實際上卷谈,我們就是通過賬號和密碼來獲取token杯拐。

當(dāng)然,我們不希望用戶每次使用我們的工具時世蔗,都需要輸入賬號和密碼端逼。相反,我們將保存OAuth令牌用于后續(xù)的請求污淋。這就要用到configstore這個包啦顶滩。

九、保存配置

保存配置信息表面上看很簡單寸爆,你可以簡單的讀寫一個JSON文件就好了礁鲁。但是,configstore這個包有以下優(yōu)勢:

  1. 它會根據(jù)你的操作系統(tǒng)和當(dāng)前用戶來決定最佳的文件存儲位置赁豆;
  2. 不需要讀取文件仅醇,只需要修改configstore對象即可,后面的事他幫你搞定魔种;

用法很簡單析二,創(chuàng)建一個實例,傳入一個標(biāo)識符即可节预,例如:

const Configstore = require('configstore');
const conf = new Configstore('ginit');

如果configstore文件不存在叶摄,他會返回一個空對象并且在后臺創(chuàng)建一個文件。如果文件存在安拟,你可以直接利用里面的內(nèi)容蛤吓。你現(xiàn)在可以根據(jù)需要直接修改conf對象的屬性。同時去扣,你也不需要擔(dān)心怎么去保存它柱衔,它自己會處理好的。

提示:在macOS系統(tǒng)上愉棱,文件將會保存在/Users/[YOUR-USERNME]/.config/configstore/ginit.json下唆铐。在Linux系統(tǒng)上文件保存在/home/[YOUR-USERNME]/.config/configstore/ginit.json

十奔滑、與GitHub API通信

讓我們來創(chuàng)建一個文件來處理GitHub Token艾岂。創(chuàng)建lib/github.js并將下列代碼拷入:

// github.js

const CLI = require('clui');
const Configstore = require('configstore');
const Spinner = CLI.Spinner;
const { Octokit } = require("@octokit/rest")
const { createBasicAuth } = require('@octokit/auth-basic');

const inquirer = require('./inquirer');
const pkg = require('../package.json');
// 初始化本地的存儲配置
const conf = new Configstore(pkg.name);

現(xiàn)在讓我們來創(chuàng)建一個函數(shù)來檢查我們是否擁有token。我們還創(chuàng)建了一個函數(shù)朋其,方便其他模塊獲取到octokit實例王浴。在lib/github.js中增加下列代碼:

// github.js

// ...初始化

// 模塊內(nèi)部的單例
let octokit;

module.exports = {
    // 獲取octokit實例
    getInstance: () => {
        return octokit;
    },

    // 獲取本地token
    getStoredGithubToken: () => {
        return conf.get('github.token');
    }
}

如果conf對象存在且github.token屬性也存在,就表示token存在梅猿。這里我們就可以把token返回給調(diào)用函數(shù)氓辣。我們稍后會講它。

如果沒檢查到token袱蚓,則需要去獲取一個钞啸。當(dāng)然,獲取OAuth token涉及到網(wǎng)絡(luò)請求喇潘,這意味著用戶需要短暫的等待体斩。借此機會,我們可以看到clui提供控制帶UI增強功能颖低,loading效果就是其中一個絮吵。

創(chuàng)建一個loading效果很簡單:

const status = new Spinner('Authenticating you, please wait...');
status.start();

完成后,只需要停止他忱屑,他就會在屏幕上消失:

status.stop();

提示:你也可以用update來動態(tài)的設(shè)置文字蹬敲。如果你需要一個進(jìn)度指示器,例如展示當(dāng)前的進(jìn)度的百分比莺戒,這可能非常有用粱栖。

將下面代碼拷貝到lib/github.js中,這是完成GitHub認(rèn)證的代碼:

// github.js

module.exports = {
    // 獲取實例
    getInstance: () => { ... },

    // 獲取本地token
    getStoredGithubToken: () => { ... },

    // 通過個人賬號信息獲取token
    getPersonalAccessToken: async () => {
        const credentials = await inquirer.askGithubCredentials();
        const status = new Spinner('驗證身份中脏毯,請等待...');

        status.start();

        const auth = createBasicAuth({
            username: credentials.username,
            password: credentials.password,
            async on2Fa() {
                // 等待實現(xiàn)
            },
            token: {
                scopes: ['user', 'public_repo', 'repo', 'repo:status'],
                note: 'ginit, the command-line tool for initalizing Git repos',
            },
        });

        try {
            const res = await auth();

            if (res.token) {
                conf.set('github.token', res.token);
                return res.token;
            } else {
                throw new Error('獲取GitHub token失敗');
            }
        } finally {
            status.stop();
        }
    }
};

讓我們來逐步完成:

  1. 用之前我們定義的函數(shù)askGithubCredentials來詢問用戶的賬號和密碼闹究;
  2. 我們使用createBasicAuth來創(chuàng)建一個auth函數(shù),方便后面調(diào)用食店。需要給這個函數(shù)傳遞用戶的用戶名和密碼渣淤,同時還需要傳遞一個token對象,它擁有下面2個屬性:
    1. note:記錄獲取token的用途吉嫩;
    2. scopes:一個授權(quán)信息使用范圍的列表价认,你可以在GitHub上了解更多信息;
  3. 我們將會try catch中利用await語法等待函數(shù)的返回結(jié)果自娩;
  4. 如果授權(quán)成功用踩,我們將會獲取到token渠退,可以把它放到configstore中,方便下次直接使用脐彩;
  5. 如果因為某些原因?qū)е率跈?quán)失敗碎乃,我們將在捕捉到它,根據(jù)狀態(tài)碼處理異常的情況惠奸;

您創(chuàng)建的任何token(無論是手動創(chuàng)建的還是通過API生成的)都可以在此處看到梅誓。 在開發(fā)過程中,你可能需要刪除ginittoken(可以通過上面提供的note參數(shù)識別)佛南,以便重新生成它梗掰。

更新index.js中的代碼:

// index.js

const github = require('./lib/github');

...

const run = async () => {
    // 從本地獲取token記錄
    let token = github.getStoredGithubToken();
    if(!token) {
        // 通過賬號、密碼獲取token
        token = await github.getPersonalAccessToken();
    }
    console.log(token);
};

第一次運行時嗅回,你需要輸入你的用戶名和密碼及穗。我們將會在github上創(chuàng)建一個token并把它保存起來。下次運行時绵载,我們將直接使用保存起來的token做身份認(rèn)證拥坛。

十一、處理雙重認(rèn)證

希望你注意到上面代碼中的on2Fa函數(shù)尘分。當(dāng)用戶的賬號使用雙重認(rèn)證時猜惋,將會調(diào)用到這個函數(shù)。讓我們在lib/inquirer.js中插入如下代碼:

// inquirer.js

const inquirer = require('inquirer');

module.exports = {
    // 詢問git賬號信息
    askGithubCredentials: () => { ... },

    // 詢問雙重認(rèn)證碼
    getTwoFactorAuthenticationCode: () => {
        return inquirer.prompt({
            name: 'twoFactorAuthenticationCode',
            type: 'input',
            message: '請輸入你的雙重認(rèn)證驗證碼:',
            validate: function (value) {
                if (value.length) {
                    return true;
                } else {
                    return '請輸入你的雙重認(rèn)證驗證碼:.';
                }
            },
        });
    }
}

修改lib/github.js中的on2Fa函數(shù):

// github.js

async on2Fa() {
    status.stop();
    const res = await inquirer.getTwoFactorAuthenticationCode();
    status.start();
    return res.twoFactorAuthenticationCode;
}

現(xiàn)在我們的程序可以處理GitHub雙重認(rèn)證培愁。

十二著摔、創(chuàng)建倉庫

獲取Oauth令牌之后,我們就可以利用它來創(chuàng)建遠(yuǎn)程倉庫定续。

同樣谍咆,我們可以利用Inquirer來問一系列問題。我們需要獲取一個倉庫名字私股,我們可以要求用戶選填一個描述摹察,還需要詢問倉庫是共有還是私有。

我們可以利用minimist來從命令行參數(shù)中獲取倉庫名稱和描述倡鲸。

ginit my-repo "just a test repository"

下面的代碼將會解析出一個數(shù)組:

const argv = require('minimist')(process.argv.slice(2));
// { _: [ 'my-repo', 'just a test repository' ] }

我們將通過代碼來實現(xiàn)上面所說的提問供嚎。首先將下列代碼拷貝到lib/inquirer.js中:

// inquirer.js

const inquirer = require('inquirer');
const files = require('./files');

module.exports = {
    // 詢問git賬號信息
    askGithubCredentials: () => { ... },

    // 詢問雙重認(rèn)證碼
    getTwoFactorAuthenticationCode: () => { ... },

    // 詢問倉庫詳細(xì)信息
    askRepoDetails: () => {
        const argv = require('minimist')(process.argv.slice(2));

        const questions = [
            {
                type: 'input',
                name: 'name',
                message: '請輸入git倉庫名稱:',
                default: argv._[0] || files.getCurrentDirectoryBase(),
                validate: function (value) {
                    if (value.length) {
                        return true;
                    } else {
                        return '請輸入git倉庫名稱.';
                    }
                },
            },
            {
                type: 'input',
                name: 'description',
                default: argv._[1] || null,
                message: '請輸入倉庫描述(選填):',
            },
            {
                type: 'list',
                name: 'visibility',
                message: '共有倉庫 或 私有倉庫:',
                choices: ['public', 'private'],
                default: 'public',
            },
        ];
        return inquirer.prompt(questions);
    }
};

創(chuàng)建lib/repo.js文件,并添加如下代碼:

// repo.js

const CLI = require('clui');
const fs = require('fs');
const git = require('simple-git/promise')();
const Spinner = CLI.Spinner;
const touch = require('touch');
const _ = require('lodash');

const inquirer = require('./inquirer');
const gh = require('./github');

module.exports = {
    // 創(chuàng)建遠(yuǎn)程倉庫
    createRemoteRepo: async () => {
        const github = gh.getInstance();
        const answers = await inquirer.askRepoDetails();

        const data = {
            name: answers.name,
            description: answers.description,
            private: answers.visibility === 'private',
        };

        const status = new Spinner('創(chuàng)建遠(yuǎn)程倉庫中...');
        status.start();

        try {
            const response = await github.repos.createForAuthenticatedUser(data);
            return response.data.ssh_url;
        } finally {
            status.stop();
        }
    }
}

獲取以上信息后峭状,我就可以創(chuàng)建Git倉庫了克滴。我們這本地將生成好的倉庫設(shè)置成我們的遠(yuǎn)程倉庫。但是优床,在這之前讓我們以交互的方式來創(chuàng)建一個.gitignore文件吧劝赔。

十三、創(chuàng)建 .gitignore

下一步胆敞,我們將要創(chuàng)建一個簡單的命令行“向?qū)А眮砩?code>.gitignore文件着帽。如果用戶在現(xiàn)有項目目錄中執(zhí)行我們的命令行工具杂伟,請向他們展示當(dāng)前目錄已經(jīng)存在的文件和目錄,并允許他們選擇需要忽略的文件和文件夾仍翰。

inquirer提供了一個復(fù)選框給我們使用赫粥。

image

我們需要掃描當(dāng)前目錄中.git.gitignore以外的文件。

const filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore');

如果沒有文件需要添加到'.gitignore'中歉备,那么直接創(chuàng)建一個.gitignore文件即可

if (filelist.length) {
  ...
} else {
  touch('.gitignore');
}

讓我們在lib/inquirer.js中添加如下代碼:

// inquirer.js

// 選擇需要忽略的文件
askIgnoreFiles: (fileList) => {
    const questions = [
        {
            type: 'checkbox',
            name: 'ignore',
            message: '請選擇你想要忽略的文件:',
            choices: fileList,
            default: ['node_modules', 'bower_components'],
        },
    ];
    return inquirer.prompt(questions);
},

注意:我們可以提供一些默認(rèn)選項傅是,如果node_modulesbower_components存在的話匪燕,我們將提前選中蕾羊。

lib/repo.js中,我們添加如下代碼:

// repo.js

// 創(chuàng)建git ignore
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 {
            // 創(chuàng)建文件
            touch('.gitignore');
        }
    } else {
        // 創(chuàng)建文件
        touch('.gitignore');
    }
},

一旦提交帽驯,我們將會根據(jù)選中的文件生成一個.gitignore文件龟再。既然已經(jīng)可以生成.gitignore文件了,讓我們初始化git倉庫吧尼变。

十四利凑、在命令行中與git交互

有很多實現(xiàn)和git交互的方法,但是嫌术,最簡單的方法可能是simple-git哀澈。這個庫提供了一批可以鏈?zhǔn)秸{(diào)用的異步函數(shù),在后臺執(zhí)行g(shù)it命令度气。

這是我們需要做的重復(fù)任務(wù):

  1. 運行git init
  2. 添加.gitignore
  3. 添加目錄中的其余文件
  4. 完成初次提交
  5. 添加新創(chuàng)建的遠(yuǎn)程倉庫
  6. 將代碼推送到遠(yuǎn)端

lib/repo.js中添加如下代碼:

// repo.js

// 設(shè)置
setupRepo: async (url) => {
    const status = new Spinner('初始化本地倉庫并推送到遠(yuǎn)端倉庫中...');
    status.start();

    try {
        await git.init();
        await git.add('.gitignore');
        await git.add('./*');
        await git.commit('Initial commit')
        await git.addRemote('origin', url);
        await git.push('origin', 'master');
    } finally {
        status.stop();
    }
},

十五割按、把代碼串起來

首先,我們需要在lib/github.js文件中增加一個函數(shù)磷籍,該函數(shù)的作用是建立一個oauth認(rèn)證:

// github.js

// 通過token登陸
githubAuth: (token) => {
    octokit = new Octokit({
        auth: token,
    });
},

然后适荣,我們需要創(chuàng)建一個函數(shù)來控制獲取token的邏輯。在run函數(shù)前院领,增加如下代碼:

// index.js

// 獲取github token
const getGithubToken = async () => {
    // 從本地獲取token記錄
    let token = github.getStoredGithubToken();
    if (token) {
        return token;
    }

    // 通過賬號弛矛、密碼獲取token
    token = await github.getPersonalAccessToken();
    return token;
};

最后,我們用下面的代碼來更新我們的run函數(shù):

// index.js

const run = async () => {
    try {
        // 獲取token
        const token = await getGithubToken();
        github.githubAuth(token);

        // 創(chuàng)建遠(yuǎn)程倉庫
        const url = await repo.createRemoteRepo();

        // 創(chuàng)建 .gitignore
        await repo.createGitignore();

        // 初始化本地倉庫并推送到遠(yuǎn)端
        await repo.setupRepo(url);

        console.log(chalk.green('All done!'));
    } catch (err) {
        if (err) {
            switch (err.status) {
                case 401:
                    console.log(chalk.red("登陸失敗比然,請?zhí)峁┱_的登陸信息"));
                    break;
                case 422:
                    console.log(chalk.red('遠(yuǎn)端已存在同名倉庫'));
                    break;
                default:
                    console.log(chalk.red(err));
            }
        }
    }
};

如你所見丈氓,在調(diào)用我們其他的函數(shù)之前(createRemoteRepo(), createGitignore(), setupRepo()),我們確保用戶已經(jīng)通過了身份驗證强法。而且扒寄,還處理任何錯誤并且給用戶適當(dāng)?shù)姆答仭?/p>

你可以在git倉庫中找到完整的代碼。

現(xiàn)在拟烫,你就擁有了一個可以運行的命令行工具了该编。運行一下,看看他是不是按照你的預(yù)期工作硕淑。

十六课竣、讓ginit命令全局可用

還有一件需要做的事就是讓我們的命令行全局可用嘉赎。為了實現(xiàn)這個事,我們需要在index.js文件頭部加上shebang于樟。

#!/usr/bin/env node

然后公条,我們需要在package.json中增加一個bin屬性。用于綁定命令名稱ginit和被執(zhí)行的文件迂曲。

"bin": {
  "ginit": "./index.js"
}

然后全局安裝模塊靶橱,命令行工具就可以用了。

npm install -g

如果你想確認(rèn)安裝是否生效路捧,你可以把本機上全局安裝的node模塊列出來看看:

npm ls -g --depth=0

十七关霸、展望

我們已經(jīng)創(chuàng)建了一個漂亮且簡潔的初始化Git倉庫的命令行工具。而且你還可以做很多事情去提升它杰扫。

如果你是一個Bitbucket用戶队寇,你可以利用Bitbucket API給這個命令行增加一個創(chuàng)建Bitbucket倉庫的功能。這個node包 bitbucket-api對你會有幫助章姓。你可以增加另外一個命令行選項或者詢問用戶是否要使用Bitbucket佳遣,或者直接把現(xiàn)在處理GitHub的代碼替換成Bitbucket

你可以提供一個.gitignore默認(rèn)的文件集合凡伊,而不是硬編碼零渐。preferences這個包很適合這個場景,或者你可以提供一個模版系忙,提示用戶輸入對應(yīng)的模版類型即可诵盼。也可以把它集成到.gitignore.io中。

除此之外笨觅,你還可以增加其他驗證拦耐,提供跳過某些步驟的功能等等。

這是一篇老文章了见剩,不過杀糯,今年2月份作者又更新了一部分內(nèi)容,剔除了其中失效的依賴苍苞。同時固翰,在閱讀的過程中,我也優(yōu)化了一下示例代碼羹呵。

關(guān)于我

我是一個莫得感情的代碼搬運工骂际,每周會更新1至2篇前端相關(guān)的文章,有興趣的老鐵可以掃描下面的二維碼關(guān)注或者直接微信搜索前端補習(xí)班關(guān)注冈欢。

image

精通前端很難歉铝,讓我們來一起補補課吧!

好啦凑耻,翻譯完畢啦太示,原文鏈接在此 Build a JavaScript Command Line Interface (CLI) with Node.js柠贤。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市类缤,隨后出現(xiàn)的幾起案子臼勉,更是在濱河造成了極大的恐慌,老刑警劉巖餐弱,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宴霸,死亡現(xiàn)場離奇詭異,居然都是意外死亡膏蚓,警方通過查閱死者的電腦和手機瓢谢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來降允,“玉大人恩闻,你說我怎么就攤上這事艺糜【缍” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵破停,是天一觀的道長翅楼。 經(jīng)常有香客問我,道長真慢,這世上最難降的妖魔是什么毅臊? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮黑界,結(jié)果婚禮上管嬉,老公的妹妹穿的比我還像新娘。我一直安慰自己朗鸠,他們只是感情好蚯撩,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著烛占,像睡著了一般胎挎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上忆家,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天犹菇,我揣著相機與錄音,去河邊找鬼芽卿。 笑死揭芍,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的卸例。 我是一名探鬼主播称杨,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼流酬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了列另?” 一聲冷哼從身側(cè)響起芽腾,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎页衙,沒想到半個月后摊滔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡店乐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年艰躺,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片眨八。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡腺兴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出廉侧,到底是詐尸還是另有隱情页响,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布段誊,位于F島的核電站闰蚕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏连舍。R本人自食惡果不足惜没陡,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望索赏。 院中可真熱鬧盼玄,春花似錦、人聲如沸潜腻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽砾赔。三九已至蝌箍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間暴心,已是汗流浹背妓盲。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留专普,地道東北人悯衬。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親筋粗。 傳聞我的和親對象是個殘疾皇子策橘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360