Skip to content

手把手教你写一个 Node.js CLI #7

Description

@GeSnowBoy

强大的 Node.js 除了能写传统的 Web 应用,其实还有更广泛的用途。微服务、REST API、各种工具……甚至还能开发物联网和桌面应用。JavaScript 不愧是宇宙第一语言。

Node.js 在开发命令行工具方面也是相当方便,通过这篇教程我们可以来感受下。我们先看看跟命令行有关的几个第三方包,然后从零开始写一个真实的命令行工具。

这个 CLI 的用途就是初始化一个 Git 仓库。当然,底层就是调用了 git init,但是它的功能不止这么简单。它还能从命令行创建一个远程的 Github 仓库,允许用户交互式地创建 .gitignore文件,最后还能完成提交和推代码。

![](//upload-images.jianshu.io/upload_images/1618526-4f40d20b011b2e65.png)
写一个 Node CLI 总共分几步?

为什么要用 Node.js 写命令行工具

在动手之前,我们有必要知道为什么选择 Node.js 开发命令行工具。

最明显优势就是——相信你已经猜到了——它是用 JavaScript 写的。

另外一个原因是 Node.js 生态系统非常完善,各种用途的 package 应有尽有,其中就有不少是专门为了开发命令行工具的。

最后一个原因是,用npm 管理依赖不用担心跨平台问题,不像 Aptitude、Yum 或者 Homebrew 这些针对特定操作系统的包管理工具,令人头疼。

注:这么说不一定准确,可能命令行只是需要其他的外部依赖。

动手写一个命令行工具: ginit

![](//upload-images.jianshu.io/upload_images/1618526-e327844d86964d07.png)
Ginit, our Node CLI in action

在这篇教程里我们来开发一个叫做 ginit 的命令行工具。我们可以把它当做高配版的git init。什么意思呢?我们都知道,git init 命令会在当前目录初始化一个 git 仓库。但是,这仅仅是创建或关联已有项目到 Git 仓库的其中一步而已。典型的工作流程是这样的:

  1. 运行git init初始化本地仓库
  2. 创建远程仓库(比如在 Github 或 Bitbucket 上)——这一步通常要脱离命令行,打开浏览器来操作
  3. 添加 remote
  4. 创建.gitignore文件
  5. 添加项目文件
  6. commit 本地文件
  7. push 到远程

可能还有更多步骤,为了演示我们只看关键部分。你会发现,这些步骤很多都是机械式、重复性的,为什么不用命令行来完成这些工作呢?比如复制粘贴 git 地址这种事情,能忍受手动操作?

ginit 可以做到:在当前目录创建 git 仓库,同时创建远程仓库(我们这里用 Github 演示),然后提供一个类似操作向导的界面来创建 .gitignore 文件,最后提交文件夹内容并推送到远程仓库。可能这也节省不了太多时间,但是它确实给创建新项目带来了些许便利。

好了,我们开始吧。

项目依赖

有一点是肯定的:说到外观,控制台无论如何也不会有图形界面那么复杂。尽管如此,也并不是说控制台一定是那种原始的纯文本丑陋界面。你会惊讶地发现,原来命令行也可以那么好看!我们会用到一些美化命令行界面的库: chalk 给输出内容着色, clui 提供一些可视化组件。还有更好玩的, figlet 可以生成炫酷的 ASCII 字符图案, clear 用来清除控制台。

输入输出方面,低端的 Readline Node.js 模块可以询问用户并接受输入,简单场景下够用了。但我们会用到一个更高端的工具—— Inquirer。除了询问用户的功能,它还提供简单的输入控件:单选框和复选框,这可是在命令行控制台啊,有点意外吧。

我们还用到 minimist 来解析命令行参数。

以下是完整列表:

  • chalk :彩色输出
  • clear : 清空命令行屏幕
  • clui :绘制命令行中的表格、仪表盘、加载指示器等。
  • figlet :生成字符图案
  • inquirer :创建交互式的命令行界面
  • minimist :解析参数
  • configstore:轻松加载和保存配置

还有这些:

  • @octokit/rest:Node.js 里的 GitHub REST API 客户端
  • lodash:JavaScript 工具库
  • simple-git:在 Node.js 应用程序中运行 Git 命令的工具
  • touch:实现 Unix touch 命令的工具

开始

创建一个项目文件夹。

mkdir ginit
<span class="hljs-built_in">cd</span> ginit
`</pre>

新建一个`package.json`文件:

<pre class="hljs undefined">`npm init
`</pre>

根据提示一路往下走:

<pre class="hljs css">`<span class="hljs-selector-tag">name</span>: (<span class="hljs-selector-tag">ginit</span>)
<span class="hljs-selector-tag">version</span>: (1<span class="hljs-selector-class">.0</span><span class="hljs-selector-class">.0</span>)
<span class="hljs-selector-tag">description</span>: "<span class="hljs-selector-tag">git</span> <span class="hljs-selector-tag">init</span>" <span class="hljs-selector-tag">on</span> <span class="hljs-selector-tag">steroids</span>
<span class="hljs-selector-tag">entry</span> <span class="hljs-selector-tag">point</span>: (<span class="hljs-selector-tag">index</span><span class="hljs-selector-class">.js</span>)
<span class="hljs-selector-tag">test</span> <span class="hljs-selector-tag">command</span>:
<span class="hljs-selector-tag">git</span> <span class="hljs-selector-tag">repository</span>:
<span class="hljs-selector-tag">keywords</span>: <span class="hljs-selector-tag">Git</span> <span class="hljs-selector-tag">CLI</span>
<span class="hljs-selector-tag">author</span>: <span class="hljs-selector-attr">[YOUR NAME]</span>
<span class="hljs-selector-tag">license</span>: (<span class="hljs-selector-tag">ISC</span>)

`</pre>

安装依赖:

<pre class="hljs css">`<span class="hljs-selector-tag">npm</span> <span class="hljs-selector-tag">install</span> <span class="hljs-selector-tag">chalk</span> <span class="hljs-selector-tag">clear</span> <span class="hljs-selector-tag">clui</span> <span class="hljs-selector-tag">figlet</span> <span class="hljs-selector-tag">inquirer</span> <span class="hljs-selector-tag">minimist</span> <span class="hljs-selector-tag">configstore</span> @<span class="hljs-keyword">octokit</span>/<span class="hljs-keyword">rest</span> lodash simple-git touch --save

`</pre>

最终生成的  `package.json` 文件大概是这样的:

<pre class="hljs json">`{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"ginit"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"keywords"</span>: [
    <span class="hljs-string">"Git"</span>,
    <span class="hljs-string">"CLI"</span>
  ],
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"bin"</span>: {
    <span class="hljs-attr">"ginit"</span>: <span class="hljs-string">"./index.js"</span>
  },
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"@octokit/rest"</span>: <span class="hljs-string">"^14.0.5"</span>,
    <span class="hljs-attr">"chalk"</span>: <span class="hljs-string">"^2.3.0"</span>,
    <span class="hljs-attr">"clear"</span>: <span class="hljs-string">"0.0.1"</span>,
    <span class="hljs-attr">"clui"</span>: <span class="hljs-string">"^0.3.6"</span>,
    <span class="hljs-attr">"configstore"</span>: <span class="hljs-string">"^3.1.1"</span>,
    <span class="hljs-attr">"figlet"</span>: <span class="hljs-string">"^1.2.0"</span>,
    <span class="hljs-attr">"inquirer"</span>: <span class="hljs-string">"^5.0.1"</span>,
    <span class="hljs-attr">"lodash"</span>: <span class="hljs-string">"^4.17.4"</span>,
    <span class="hljs-attr">"minimist"</span>: <span class="hljs-string">"^1.2.0"</span>,
    <span class="hljs-attr">"simple-git"</span>: <span class="hljs-string">"^1.89.0"</span>,
    <span class="hljs-attr">"touch"</span>: <span class="hljs-string">"^3.1.0"</span>
  }
}

`</pre>

然后在这个文件夹里新建一个`index.js`文件,加上这段代码:

<pre class="hljs javascript">`<span class="hljs-keyword">const</span> chalk       = <span class="hljs-built_in">require</span>(<span class="hljs-string">'chalk'</span>);
<span class="hljs-keyword">const</span> clear       = <span class="hljs-built_in">require</span>(<span class="hljs-string">'clear'</span>);
<span class="hljs-keyword">const</span> figlet      = <span class="hljs-built_in">require</span>(<span class="hljs-string">'figlet'</span>);
`</pre>

#### 添加一些 Helper 方法

接下来新建一个`lib`文件夹,用来存放各种 helper 模块:
  • files.js — 基本的文件管理

  • inquirer.js — 命令行用户界面

  • github.js — access token 管理

  • repo.js — Git 仓库管理

    先看 lib/files.js,这里需要完成:

  • 获取当前路径(文件夹名作为默认仓库名)

  • 检查路径是否存在(通过查找名为.git的目录,判断当前目录是否已经是 Git 仓库)

    看上去很简单直接,但这里还是有点坑的。

    首先,你可能想用fs模块的 realpathSync 方法获取当前路径:

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

    当我们在同一路径下运行应用时(即 node index.js),这没问题。但是要知道,我们要把这个控制台应用做成全局的,就是说我们想要的是当前工作目录的名称,而不是应用的安装路径。因此,最好使用 process.cwd:

    `path.basename(process.cwd());
    `

    其次,检查文件或目录是否存在的推荐方法一直在变。目前的方法是用fs.statfs.statSync。这两个方法在文件不存在的情况下会抛异常,所以我们要用try...catch

    最后需要注意的是,当你在写命令行应用的时候,使用这些方法的同步版本就可以了。

    整理下lib/files.js代码,一个工具包就出来了:

    `const fs = require('fs');
    const path = require('path');
    
    module.exports = {
      getCurrentDirectoryBase : () => {
        return path.basename(process.cwd());
      },
    
      directoryExists : (filePath) => {
        try {
          return fs.statSync(filePath).isDirectory();
        } catch (err) {
          return false;
        }
      }
    };
    `

    回到index.js文件,引入这个文件:

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

    有了这个,我们就可以动手开发应用了。

    初始化 Node CLI

    现在让我们来实现控制台应用的启动部分。

    为了展示安装的这些控制台输出强化模块,我们先清空屏幕,然后展示一个banner:

    `clear();
    console.log(
      chalk.yellow(
        figlet.textSync('Ginit', { horizontalLayout: 'full' })
      )
    );
    
    `

    输出效果如下图:

    ![](//upload-images.jianshu.io/upload_images/1618526-c6b6784df6a2f39c.png)
    The welcome banner on our Node CLI, created using Chalk and Figlet

    接着运行简单的检查,确保当前目录不是 Git 仓库。很容易,我们只要用刚才创建的工具方法检查是否存在 .git 文件夹就行了:

    `if (files.directoryExists('.git')) {
      console.log(chalk.red('Already a git repository!'));
      process.exit();
    }
    `

    提示:我们用了 chalk 模块 来展示红色的消息。

    提示用户输入

    接下来我们要做的就是写个函数,提示用户输入 Github 登录凭证。这个可以用 Inquirer 来实现。这个模块包含一些支持各种提示类型的方法,语法上跟 HTML 表单控件类似。为了收集用户的 Github 用户名和密码,我们分别用了 inputpassword 类型。

    首先新建 lib/inquirer.js 文件,加入以下代码:

    `const inquirer   = require('inquirer');
    const files      = require('./files');
    
    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 username or e-mail address.';
              }
            }
          },
          {
            name: 'password',
            type: 'password',
            message: 'Enter your password:',
            validate: function(value) {
              if (value.length) {
                return true;
              } else {
                return 'Please enter your password.';
              }
            }
          }
        ];
        return inquirer.prompt(questions);
      },
    }
    `

    如你所见,inquirer.prompt() 向用户询问一系列问题,并以数组的形式作为参数传入。数组的每个元素都是一个对象,分别定义了nametypemessage属性。

    用户提供的输入信息返回一个 promise 给调用函数。如果成功,我们会得到一个对象,包含usernamepassword属性。

    可以在index.js里测试下:

    `const inquirer  = require('./lib/inquirer');
    
    const run = async () => {
      const credentials = await inquirer.askGithubCredentials();
      console.log(credentials);
    }
    
    run();
    
    `

    运行 node index.js

    ![](//upload-images.jianshu.io/upload_images/1618526-551395fecacf7b41.png)
    Getting user input with Inquirer

    处理 GitHub 身份验证

    下一步是创建一个函数,用来获取 Github API 的OAuth token。我们实际上是用用户名和密码换取 token的。

    当然了,我们不能让用户每次使用这个工具的时候都需要输入身份凭证,而是把 OAuth token 存起来给后续请求使用。这个是就要用到 configstore 这个包了。

    保存配置信息

    保存配置信息表面上看起来非常简单直接:无需第三方库,直接存取 JSON 文件就好了。但是,configstore 这个包还有几个关键的优势:

  1. 它会根据你的操作系统和当前用户来决定最佳的文件存储位置。

  2. 不需要直接读写文件,只要修改 configstore 对象,后面的事都帮你搞定了。

    用法也很简单,创建一个实例,传入应用标识符就行了。例如:

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

    如果 configstore 文件不存在,它会返回一个空对象并在后台创建该文件。如果 configstore 文件已经存在,内容会被解析成 JSON,应用程序就可以使用它了。 你可以把 conf 当成简单的对象,根据需要获取或设置属性。刚才已经说了,你不用担心保存的问题,它已经帮你做好了。

    提示:macOS/Linux 系统该文件位于 /Users/[YOUR-USERNME]/.config/configstore/ginit.json

    与 GitHub API 通信

    让我们写一个库,处理 GitHub token。新建文件 lib/github.js 并添加以下代码:

    `const octokit     = require('@octokit/rest')();
    const Configstore = require('configstore');
    const pkg         = require('../package.json');
    const _           = require('lodash');
    const CLI         = require('clui');
    const Spinner     = CLI.Spinner;
    const chalk       = require('chalk');
    
    const inquirer    = require('./inquirer');
    
    const conf = new Configstore(pkg.name);
    
    `

    再添加一个函数,检查访问 token 是否已经存在。我们还添加了一个函数,以便其他库可以访问到 octokit(GitHub) 相关函数:

    `...
    module.exports = {
    
      getInstance: () => {
        return octokit;
      },
    
      getStoredGithubToken : () => {
        return conf.get('github.token');
      },
    
      setGithubCredentials : async () => {
        ...
      },
    
      registerNewToken : async () => {
        ...
      }
    
    }
    
    `

    如果 conf 对象存在并且有 github.token 属性,就表示 token 已经存在。在这里我们把 token 值返回给调用的函数。我们稍后会讲到它。

    如果 token 没找到,我们需要获取它。当然了,获取 OAuth token 牵涉到网络请求,对用户来说有短暂的等待过程。借这个机会我们可以看看 clui 这个包,它给控制台应用提供了强化功能,转菊花就是其中一个。

    创建一个菊花很简单:

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

    任务完成后就可以停掉它,它就从屏幕上消失了:

    `status.stop();
    
    `

    提示:你也可以用update方法动态更新文字内容。当你需要展示进度时这会非常有用,比如显示完成的百分比。

    完成 GitHub 认证的代码在这:

    `...
      setGithubCredentials : async () => {
        const credentials = await inquirer.askGithubCredentials();
        octokit.authenticate(
          _.extend(
            {
              type: 'basic',
            },
            credentials
          )
        );
      },
    
      registerNewToken : async () => {
        const status = new Spinner('Authenticating you, please wait...');
        status.start();
    
        try {
          const response = await octokit.authorization.create({
            scopes: ['user', 'public_repo', 'repo', 'repo:status'],
            note: 'ginits, the command-line tool for initalizing Git repos'
          });
          const token = response.data.token;
          if(token) {
            conf.set('github.token', token);
            return token;
          } else {
            throw new Error("Missing Token","GitHub token was not found in the response");
          }
        } catch (err) {
          throw err;
        } finally {
          status.stop();
        }
      },
    
    `

    我们一步一步来看:

  3. 用之前定义的 setGithubCredentials 方法提示用户输入凭证

  4. 试图获取 OAuth token之前采用 basic authentication

  5. 尝试注册新的 token

  6. 如果成功获取了 token,保存到 configstore

  7. 返回 token

    你创建的任何 token,无论是通过人工还是 API,都可以在 这里看到。在开发过程中,你可能需要删除 ginit 的 access token ——可以通过上面的 note 参数辨认—— 以便重新生成。

    提示:如果你的 Github 账户启用了双重认证,这个过程会稍微复杂点。你需要请求验证码(比如通过手机短信),然后通过 X-GitHub-OTP 请求头提供该验证码。更多信息请参阅 Github 开发文档

    更新下index.js文件里的run()函数,看看效果:

    `const run = async () => {
      let token = github.getStoredGithubToken();
      if(!token) {
        await github.setGithubCredentials();
        token = await github.registerNewToken();    
      }
      console.log(token);
    }
    
    `

    请注意,如果某个地方出错的话,你会得到一个 Promise 错误,比如输入的密码不对。稍后我们会讲到处理这些错误的方式。

    创建仓库

    一旦获得了 OAuth token,我们就可以用它来创建远程 Github 仓库了。

    同样,我们可以用 Inquirer 给用户提问。我们需要仓库名称、可选的描述信息以及仓库是公开还是私有。

    我们用 minimist 从可选的命令行参数中提取名称和描述的默认值。例如:

    `ginit my-repo "just a test repository"
    `

    这样就设置了默认名称为my-repo ,默认描述为just a test repository

    下面这行代码把参数放在一个数组里:

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

    提示:这里只展示了 minimist 功能的一点皮毛而已。你还可以用它来解析标志位参数、开关和键值对。更多功能请查看它的文档。

    接下来我们加上解析命令行参数的代码,并向用户提出一系列问题。首先更新lib/inquirer.js文件,在askGithubCredentials函数后面加上以下代码:

    `  ...
      askRepoDetails: () => {
        const argv = require('minimist')(process.argv.slice(2));
    
        const questions = [
          {
            type: 'input',
            name: 'name',
            message: 'Enter a name for the repository:',
            default: argv._[0] || files.getCurrentDirectoryBase(),
            validate: function( value ) {
              if (value.length) {
                return true;
              } else {
                return 'Please enter a name for the repository.';
              }
            }
          },
          {
            type: 'input',
            name: 'description',
            default: argv._[1] || null,
            message: 'Optionally enter a description of the repository:'
          },
          {
            type: 'list',
            name: 'visibility',
            message: 'Public or private:',
            choices: [ 'public', 'private' ],
            default: 'public'
          }
        ];
        return inquirer.prompt(questions);
      },
    
    `

    接着创建lib/repo.js 文件,加上这些代码:

    `const _           = require('lodash');
    const fs          = require('fs');
    const git         = require('simple-git')();
    const CLI         = require('clui')
    const Spinner     = CLI.Spinner;
    
    const inquirer    = require('./inquirer');
    const gh          = require('./github');
    
    module.exports = {
      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('Creating remote repository...');
        status.start();
    
        try {
          const response = await github.repos.create(data);
          return response.data.ssh_url;
        } catch(err) {
          throw err;
        } finally {
          status.stop();
        }
      },
    }
    
    `

    根据获取的信息,我们就可以利用 Github 包 创建仓库了,它会返回新创建的仓库 URL。然后我们就可以把这个地址设置为本地仓库的 remote。不过还是先新建一个.gitignore 文件吧。

    创建 .gitignore 文件

    下一步我们将要创建一个简单的“向导”命令行,用来生成 .gitignore 文件。如果用户在已有项目路径里运行我们的应用程序,我们给用户列出当前工作目录的文件和目录, 以让他们选择忽略哪些。

    Inquirer 提供的 checkbox 输入类型就是用来做这个的。

    ![](//upload-images.jianshu.io/upload_images/1618526-2e12d56a10340bdc.png)
    Inquirer’s checkboxes in action

    首先我们需要做的就是扫描当前目录,忽略.git文件夹和任何现有的 .gitignore 文件。我们用 lodash 的 without 方法来做:

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

    如果没有符合条件的结果,就没必要继续执行了,直接 touch 当前的.gitignore 文件并退出函数。

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

    最后,我们用 Inquirer’s 的 checkbox 列出所有文件。在 lib/inquirer.js 加上如下代码:

    `...
      askIgnoreFiles: (filelist) => {
        const questions = [
          {
            type: 'checkbox',
            name: 'ignore',
            message: 'Select the files and/or folders you wish to ignore:',
            choices: filelist,
            default: ['node_modules', 'bower_components']
          }
        ];
        return inquirer.prompt(questions);
      },
    ..
    
    `

    请注意,我们也可以提供默认忽略列表。在这里我们预先选择了 node_modulesbower_components 目录,如果存在的话。

    有了 Inquirer 的代码,现在我们可以写 createGitignore() 函数了。在 lib/repo.js 文件里插入这些代码:

    `...
      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( '.gitignore' );
          }
        } else {
            touch('.gitignore');
        }
      },
    ...
    
    `

    一旦用户确认,我们把选中的文件列表用换行符拼接起来,写入 .gitignore 文件。 有了.gitignore 文件,可以初始化 Git 仓库了。

    应用程序中的 Git 操作

    操作 Git 的方法有很多,最简单的可能就是使用 simple-git 了。它提供一系列的链式方法运行 Git 命令。

    我们用它来自动化的重复性任务有这些:

  8. 运行 git init

  9. 添加 .gitignore 文件

  10. 添加工作目录的其余内容

  11. 执行初次 commit

  12. 添加新创建的远程仓库

  13. push 工作目录到远端

    lib/repo.js 中插入以下代码:

    `...
      setupRepo: async (url) => {
        const status = new Spinner('Initializing local repository and pushing to remote...');
        status.start();
    
        try {
          await git
            .init()
            .add('.gitignore')
            .add('./*')
            .commit('Initial commit')
            .addRemote('origin', url)
            .push('origin', 'master');
          return true;
        } catch(err) {
          throw err;
        } finally {
          status.stop();
        }
      },
    ...
    
    `

    全部串起来

    首先在 lib/github.js中写几个 helper 函数。一个用来方便地存取 token,一个用来建立 oauth认证:

    `...
      githubAuth : (token) => {
        octokit.authenticate({
          type : 'oauth',
          token : token
        });
      },
    
      getStoredGithubToken : () => {
        return conf.get('github.token');
      },
    ...
    
    `

    接着在 index.js里写个函数用来处理获取 token 的逻辑。在run()函数前加入这些代码:

    `const getGithubToken = async () => {
      //从 config store 获取 token
      let token = github.getStoredGithubToken();
      if(token) {
        return token;
      }
    
      // 没找到 token ,使用凭证访问 GitHub 账号
      await github.setGithubCredentials();
    
      // 注册新 token
      token = await github.registerNewToken();
      return token;
    }
    
    `

    最后,更新run() 函数,加上应用程序主要逻辑处理代码。

    `const run = async () => {
      try {
        // 获取并设置认证 Token
        const token = await getGithubToken();
        github.githubAuth(token);
    
        // 创建远程仓库
        const url = await repo.createRemoteRepo();
    
        // 创建 .gitignore 文件
        await repo.createGitignore();
    
        // 建立本地仓库并推送到远端
        const done = await repo.setupRepo(url);
        if(done) {
          console.log(chalk.green('All done!'));
        }
      } catch(err) {
          if (err) {
            switch (err.code) {
              case 401:
                console.log(chalk.red('Couldn\'t log you in. Please provide correct credentials/token.'));
                break;
              case 422:
                console.log(chalk.red('There already exists a remote repository with the same name'));
                break;
              default:
                console.log(err);
            }
          }
      }
    }
    
    `

    如你所见,在顺序调用其他函数(createRemoteRepo(), createGitignore(), setupRepo())之前,我们要确保用户是通过认证的。代码还处理了异常,并给予了用户适当的反馈。

    让 ginit 命令全局可用

    剩下的一件事是让我们的命令行在全局可用。为此,我们需要在index.js文件顶部加上一行叫 shebang 的代码:

    `#!/usr/bin/env node
    `

    接着在package.json 文件中新增一个 bin 属性。它用来绑定命令名称(ginit)和对应被执行的文件(路径相对于 package.json)。

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

    然后,在全局安装这个模块,这样一个可用的 shell 命令就生成了。

    `npm install -g
    
    

提示:Windows下也是有效的, 因为 npm 会帮你的脚本安装一个 cmd 外壳程序

更进一步

我们已经做出了一个漂亮却很简单的命令行应用程序用来初始化 Git 仓库。但是你还可以做很多事来进一步加强它。

如果你是 Bitbucket 用户,你可以适配该程序去使用 Bitbucket API 来创建仓库。有个 [Node.js API (https://www.npmjs.com/package/bitbucket-api) 可以帮你起步。你可能希望增加几个命令行选项,或者让用户选择使用 Github 还是 Bitbucket(用 Inquirer 再适合不过了),或者直接把 Github 相关的代码替换成 Bitbucket 对应的代码。

你还可以指定.gitgnore 文件默认列表,这方面 preferences 包比较合适,或者可以提供一些模板—— 可能是让用户选择项目类型。还可以把它集成到 .gitignore.io

除此之外,你还可以添加额外的验证、提供跳过某些步骤的功能等等。发挥你的想象力,如果还有其他想法,欢迎留言评论!

![](//upload-images.jianshu.io/upload_images/1618526-f804a25c5c254fb9.png)
微信公众号:1024 译站

Metadata

Metadata

Assignees

No one assigned

    Labels

    前端前端标签

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions