├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .mocharc.yml ├── LICENSE ├── README.md ├── bin └── hexo ├── completion ├── README.md ├── bash ├── fish └── zsh ├── lib ├── console │ ├── help.ts │ ├── index.ts │ ├── init.ts │ └── version.ts ├── context.ts ├── extend │ └── console.ts ├── find_pkg.ts ├── goodbye.ts ├── hexo.ts └── types.ts ├── package.json ├── test ├── .eslintrc.json └── scripts │ ├── console.ts │ ├── context.ts │ ├── find_pkg.ts │ ├── help.ts │ ├── hexo.ts │ ├── init.ts │ └── version.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | 7 | [*.{js,json}] 8 | indent_style = space 9 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | tmp/ 4 | assets/ 5 | dist/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "hexo/ts.js", 4 | "parserOptions": { 5 | "sourceType": "module", 6 | "ecmaVersion": 2020 7 | }, 8 | "rules": { 9 | "@typescript-eslint/no-var-requires": 0 10 | } 11 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: gitsubmodule 8 | directory: "/" 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | pull_request: 8 | 9 | env: 10 | default_node_version: 14 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | needs: build 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-latest, macos-latest] 20 | node-version: ["14", "16", "18"] 21 | fail-fast: false 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Setup Node.js ${{ matrix.node-version }} and Cache 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: npm 29 | cache-dependency-path: "package.json" 30 | 31 | - name: Install Dependencies 32 | run: npm install 33 | - name: Test 34 | run: npm run test 35 | 36 | coverage: 37 | name: Coverage 38 | needs: build 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Setup Node.js ${{env.default_node_version}} and Cache 43 | uses: actions/setup-node@v4 44 | with: 45 | node-version: ${{env.default_node_version}} 46 | cache: npm 47 | cache-dependency-path: "package.json" 48 | 49 | - name: Install Dependencies 50 | run: npm install 51 | - name: Coverage 52 | run: npm run test-cov 53 | - name: Coveralls 54 | uses: coverallsapp/github-action@v2 55 | with: 56 | github-token: ${{ secrets.github_token }} 57 | 58 | build: 59 | name: Build 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v4 63 | - name: Setup Node.js ${{env.default_node_version}} and Cache 64 | uses: actions/setup-node@v4 65 | with: 66 | node-version: ${{env.default_node_version}} 67 | cache: npm 68 | cache-dependency-path: "package.json" 69 | 70 | - name: Install Dependencies 71 | run: npm install 72 | - name: Build 73 | run: npm run build 74 | 75 | lint: 76 | name: Lint 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/checkout@v4 80 | - name: Setup Node.js ${{env.default_node_version}} and Cache 81 | uses: actions/setup-node@v4 82 | with: 83 | node-version: ${{env.default_node_version}} 84 | cache: npm 85 | cache-dependency-path: "package.json" 86 | 87 | - name: Install Dependencies 88 | run: npm install 89 | - name: Lint 90 | run: npm run eslint 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tmp/ 4 | *.log 5 | .idea/ 6 | .nyc_output/ 7 | .vscode/ 8 | dist/ 9 | package-lock.json 10 | coverage/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "assets"] 2 | path = assets 3 | url = https://github.com/hexojs/hexo-starter.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | color: true 2 | reporter: spec 3 | parallel: false 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Tommy Chen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hexo-cli 2 | 3 | [![CI](https://github.com/hexojs/hexo-cli/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/hexojs/hexo-cli/actions/workflows/ci.yml) 4 | [![NPM version](https://badge.fury.io/js/hexo-cli.svg)](https://www.npmjs.com/package/hexo-cli) 5 | [![Coverage Status](https://coveralls.io/repos/github/hexojs/hexo-cli/badge.svg)](https://coveralls.io/github/hexojs/hexo-cli) 6 | 7 | Command line interface for Hexo. 8 | 9 | ## Installation 10 | 11 | ```bash 12 | $ npm install hexo-cli -g 13 | ``` 14 | 15 | ## Development 16 | 17 | 1. Fork this project 18 | 2. `$ git clone https://github.com/user/hexo-cli --recurse-submodules && cd hexo-cli/` 19 | 3. `$ npm install` 20 | 4. `$ npm test` 21 | 22 | ## License 23 | 24 | [MIT](LICENSE) 25 | -------------------------------------------------------------------------------- /bin/hexo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | require('../dist/hexo')(); 6 | -------------------------------------------------------------------------------- /completion/README.md: -------------------------------------------------------------------------------- 1 | # Completion for Hexo 2 | 3 | ## Bash 4 | 5 | Add the following snippet to `~/.bashrc`. 6 | 7 | ``` sh 8 | eval "$(hexo --completion=bash)" 9 | ``` 10 | 11 | ## Zsh 12 | 13 | Add the following snippet to `~/.zshrc`. 14 | 15 | ``` sh 16 | eval "$(hexo --completion=zsh)" 17 | ``` 18 | 19 | ## Fish 20 | 21 | Add the following snippet to `~/.config/fish/config.fish`. 22 | 23 | ``` sh 24 | hexo --completion=fish | source 25 | ``` 26 | -------------------------------------------------------------------------------- /completion/bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Borrowed from grunt-cli 4 | # https://gruntjs.com/ 5 | # 6 | # Copyright (c) 2012 Tyler Kellen, contributors 7 | # Licensed under the MIT license. 8 | # https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT 9 | 10 | # Usage: 11 | # 12 | # To enable bash completion for hexo, add the following line (minus the 13 | # leading #, which is the bash comment character) to your ~/.bashrc file: 14 | # 15 | # eval "$(hexo --completion=bash)" 16 | 17 | # Enable bash autocompletion. 18 | function _hexo_completions() { 19 | local cur="${COMP_WORDS[COMP_CWORD]}" 20 | local compls=$(hexo --console-list) 21 | 22 | COMPREPLY=($(compgen -W "$compls" -- "$cur")) 23 | } 24 | 25 | complete -o default -F _hexo_completions hexo 26 | -------------------------------------------------------------------------------- /completion/fish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env fish 2 | 3 | # Usage: 4 | # 5 | # To enable fish completion for hexo, add the following line to 6 | # your ~/.config/fish/config.fish file: 7 | # 8 | # hexo --completion=fish | source 9 | 10 | complete -c hexo -a "(hexo --console-list)" -f 11 | -------------------------------------------------------------------------------- /completion/zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Borrowed from grunt-cli 4 | # https://gruntjs.com/ 5 | # 6 | # Copyright (c) 2012 Tyler Kellen, contributors 7 | # Licensed under the MIT license. 8 | # https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT 9 | 10 | # Usage: 11 | # 12 | # To enable zsh completion for hexo, add the following line (minus the 13 | # leading #, which is the zsh comment character) to your ~/.zshrc file: 14 | # 15 | # eval "$(hexo --completion=zsh)" 16 | 17 | # Enable zsh autocompletion. 18 | function _hexo_completion() { 19 | compls=$(hexo --console-list) 20 | completions=(${=compls}) 21 | compadd -- $completions 22 | } 23 | 24 | compdef _hexo_completion hexo 25 | -------------------------------------------------------------------------------- /lib/console/help.ts: -------------------------------------------------------------------------------- 1 | import { underline, bold } from 'picocolors'; 2 | import { readFile } from 'hexo-fs'; 3 | import { join } from 'path'; 4 | import Promise from 'bluebird'; 5 | import type Context from '../context'; 6 | import type { Callback, Store, Command } from '../types'; 7 | 8 | const COMPLETION_DIR = join(__dirname, '../../completion'); 9 | 10 | interface HelpArgs { 11 | _: string[]; 12 | v?: boolean; 13 | version?: boolean; 14 | consoleList?: boolean; 15 | completion?: string; 16 | } 17 | 18 | function helpConsole(this: Context, args: HelpArgs) { 19 | if (args.v || args.version) { 20 | return this.call('version'); 21 | } else if (args.consoleList) { 22 | return printConsoleList(this.extend.console.list()); 23 | } else if (typeof args.completion === 'string') { 24 | return printCompletion(args.completion); 25 | } 26 | 27 | const command = args._[0]; 28 | 29 | if (typeof command === 'string' && command !== 'help') { 30 | const c = this.extend.console.get(command); 31 | if (c) return printHelpForCommand(this.extend.console.alias[command], c); 32 | } 33 | 34 | return printAllHelp(this.extend.console.list()); 35 | } 36 | 37 | function printHelpForCommand(command: string, data: Callback) { 38 | const { options } = data; 39 | 40 | const desc = options.description || options.desc || data.description || data.desc; 41 | 42 | console.log('Usage: hexo', command, options.usage || ''); 43 | console.log('\nDescription:'); 44 | console.log(`${desc}\n`); 45 | 46 | if (options.arguments) printList('Arguments', options.arguments); 47 | if (options.commands) printList('Commands', options.commands); 48 | if (options.options) printList('Options', options.options); 49 | 50 | return Promise.resolve(); 51 | } 52 | 53 | function printAllHelp(list: Store) { 54 | const keys = Object.keys(list); 55 | const commands = []; 56 | const { length } = keys; 57 | 58 | for (let i = 0; i < length; i++) { 59 | const key = keys[i]; 60 | 61 | commands.push({ 62 | name: key, 63 | desc: list[key].desc 64 | }); 65 | } 66 | 67 | console.log('Usage: hexo \n'); 68 | 69 | printList('Commands', commands); 70 | 71 | printList('Global Options', [ 72 | {name: '--config', desc: 'Specify config file instead of using _config.yml'}, 73 | {name: '--cwd', desc: 'Specify the CWD'}, 74 | {name: '--debug', desc: 'Display all verbose messages in the terminal'}, 75 | {name: '--draft', desc: 'Display draft posts'}, 76 | {name: '--safe', desc: 'Disable all plugins and scripts'}, 77 | {name: '--silent', desc: 'Hide output on console'} 78 | ]); 79 | 80 | console.log('For more help, you can use \'hexo help [command]\' for the detailed information'); 81 | console.log('or you can check the docs:', underline('https://hexo.io/docs/')); 82 | 83 | return Promise.resolve(); 84 | } 85 | 86 | function printList(title: string, list: Command[]) { 87 | list.sort((a, b) => { 88 | const nameA = a.name; 89 | const nameB = b.name; 90 | 91 | if (nameA < nameB) return -1; 92 | if (nameA > nameB) return 1; 93 | 94 | return 0; 95 | }); 96 | 97 | const lengths = list.map(item => item.name.length); 98 | const maxLen = lengths.reduce((prev, current) => Math.max(prev, current)); 99 | 100 | let str = `${title}:\n`; 101 | const { length } = list; 102 | 103 | for (let i = 0; i < length; i++) { 104 | const { description = list[i].desc } = list[i]; 105 | const pad = ' '.repeat(maxLen - lengths[i] + 2); 106 | str += ` ${bold(list[i].name)}${pad}${description}\n`; 107 | } 108 | 109 | console.log(str); 110 | 111 | return Promise.resolve(); 112 | } 113 | 114 | function printConsoleList(list: Store) { 115 | console.log(Object.keys(list).join('\n')); 116 | 117 | return Promise.resolve(); 118 | } 119 | 120 | function printCompletion(type: string) { 121 | return readFile(join(COMPLETION_DIR, type)).then(content => { 122 | console.log(content); 123 | }); 124 | } 125 | 126 | export = helpConsole; 127 | -------------------------------------------------------------------------------- /lib/console/index.ts: -------------------------------------------------------------------------------- 1 | import type Context from '../context'; 2 | import helpConsole from './help'; 3 | import initConsole from './init'; 4 | import versionConsole from './version'; 5 | 6 | export = function(ctx: Context) { 7 | const { console } = ctx.extend; 8 | 9 | console.register('help', 'Get help on a command.', {}, helpConsole); 10 | 11 | console.register('init', 'Create a new Hexo folder.', { 12 | desc: 'Create a new Hexo folder at the specified path or the current directory.', 13 | usage: '[destination]', 14 | arguments: [ 15 | {name: 'destination', desc: 'Folder path. Initialize in current folder if not specified'} 16 | ], 17 | options: [ 18 | {name: '--no-clone', desc: 'Copy files instead of cloning from GitHub'}, 19 | {name: '--no-install', desc: 'Skip npm install'} 20 | ] 21 | }, initConsole); 22 | 23 | console.register('version', 'Display version information.', {}, versionConsole); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/console/init.ts: -------------------------------------------------------------------------------- 1 | import BlueBirdPromise from 'bluebird'; 2 | import { join, resolve } from 'path'; 3 | import { magenta } from 'picocolors'; 4 | import { existsSync, readdirSync, rmdir, unlink, copyDir, readdir, stat } from 'hexo-fs'; 5 | import tildify from 'tildify'; 6 | import spawn from 'hexo-util/dist/spawn'; // for rewire 7 | import { sync as commandExistsSync } from 'command-exists'; 8 | import type Context from '../context'; 9 | 10 | const ASSET_DIR = join(__dirname, '../../assets'); 11 | const GIT_REPO_URL = 'https://github.com/hexojs/hexo-starter.git'; 12 | 13 | interface InitArgs { 14 | _: string[]; 15 | install?: boolean; 16 | clone?: boolean; 17 | } 18 | 19 | async function initConsole(this: Context, args: InitArgs) { 20 | args = Object.assign({ install: true, clone: true }, args); 21 | 22 | const baseDir = this.base_dir; 23 | const target = args._[0] ? resolve(baseDir, args._[0]) : baseDir; 24 | const { log } = this; 25 | 26 | if (existsSync(target) && readdirSync(target).length !== 0) { 27 | log.fatal(`${magenta(tildify(target))} not empty, please run \`hexo init\` on an empty folder and then copy your files into it`); 28 | await BlueBirdPromise.reject(new Error('target not empty')); 29 | } 30 | 31 | log.info('Cloning hexo-starter', GIT_REPO_URL); 32 | 33 | if (args.clone) { 34 | try { 35 | await spawn('git', ['clone', '--recurse-submodules', '--depth=1', '--quiet', GIT_REPO_URL, target], { 36 | stdio: 'inherit' 37 | }); 38 | } catch (err) { 39 | log.warn('git clone failed. Copying data instead'); 40 | await copyAsset(target); 41 | } 42 | } else { 43 | await copyAsset(target); 44 | } 45 | 46 | await BlueBirdPromise.all([ 47 | removeGitDir(target), 48 | removeGitModules(target) 49 | ]); 50 | if (!args.install) return; 51 | 52 | log.info('Install dependencies'); 53 | 54 | let npmCommand = 'npm'; 55 | if (commandExistsSync('pnpm')) { 56 | npmCommand = 'pnpm'; 57 | } else if (commandExistsSync('yarn')) { 58 | npmCommand = 'yarn'; 59 | } 60 | 61 | try { 62 | if (npmCommand === 'yarn') { 63 | const yarnVer = await spawn(npmCommand, ['--version'], { 64 | cwd: target 65 | }); 66 | if (typeof yarnVer === 'string' && yarnVer.startsWith('1')) { 67 | await spawn(npmCommand, ['install', '--production', '--ignore-optional', '--silent'], { 68 | cwd: target, 69 | stdio: 'inherit' 70 | }); 71 | } else { 72 | npmCommand = 'npm'; 73 | } 74 | } else if (npmCommand === 'pnpm') { 75 | await spawn(npmCommand, ['install', '--prod', '--no-optional', '--silent'], { 76 | cwd: target, 77 | stdio: 'inherit' 78 | }); 79 | } 80 | 81 | if (npmCommand === 'npm') { 82 | await spawn(npmCommand, ['install', '--only=production', '--optional=false', '--silent'], { 83 | cwd: target, 84 | stdio: 'inherit' 85 | }); 86 | } 87 | log.info('Start blogging with Hexo!'); 88 | } catch (err) { 89 | log.warn(`Failed to install dependencies. Please run 'npm install' in "${target}" folder.`); 90 | } 91 | } 92 | 93 | async function copyAsset(target: string) { 94 | await copyDir(ASSET_DIR, target, { ignoreHidden: false }); 95 | } 96 | 97 | function removeGitDir(target: string) { 98 | const gitDir = join(target, '.git'); 99 | 100 | return stat(gitDir).catch(err => { 101 | if (err && err.code === 'ENOENT') return; 102 | throw err; 103 | }).then(stats => { 104 | if (stats) { 105 | return stats.isDirectory() ? rmdir(gitDir) : unlink(gitDir); 106 | } 107 | }).then(() => readdir(target)).map(path => join(target, path)).filter(path => stat(path).then(stats => stats.isDirectory())).each(removeGitDir); 108 | } 109 | 110 | async function removeGitModules(target: string) { 111 | try { 112 | await unlink(join(target, '.gitmodules')); 113 | } catch (err) { 114 | if (err && err.code === 'ENOENT') return; 115 | throw err; 116 | } 117 | } 118 | 119 | export = initConsole; 120 | -------------------------------------------------------------------------------- /lib/console/version.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | const pkg = require('../../package.json'); 3 | import BlueBirdPromise from 'bluebird'; 4 | import { spawn } from 'hexo-util'; 5 | import type Context from '../context'; 6 | 7 | async function versionConsole(this: Context) { 8 | const { versions, platform } = process; 9 | const keys = Object.keys(versions); 10 | 11 | if (this.version) { 12 | console.log('hexo:', this.version); 13 | } 14 | 15 | console.log('hexo-cli:', pkg.version); 16 | 17 | let osInfo: string | void | Buffer; 18 | if (platform === 'darwin') osInfo = await spawn('sw_vers', '-productVersion'); 19 | else if (platform === 'linux') { 20 | const v = await spawn('cat', '/etc/os-release'); 21 | const distro = String(v || '').match(/NAME="(.+)"/); 22 | const versionInfo = String(v || '').match(/VERSION="(.+)"/) || ['', '']; 23 | const versionStr = versionInfo !== null ? versionInfo[1] : ''; 24 | osInfo = `${distro[1]} ${versionStr}`.trim() || ''; 25 | } 26 | 27 | osInfo = `${os.platform()} ${os.release()} ${osInfo}`; 28 | console.log('os:', osInfo); 29 | 30 | for (let i = 0, len = keys.length; i < len; i++) { 31 | const key = keys[i]; 32 | console.log('%s: %s', key, versions[key]); 33 | } 34 | 35 | await BlueBirdPromise.resolve(); 36 | } 37 | 38 | export = versionConsole; 39 | -------------------------------------------------------------------------------- /lib/context.ts: -------------------------------------------------------------------------------- 1 | import logger from 'hexo-log'; 2 | import { underline } from 'picocolors'; 3 | import Promise from 'bluebird'; 4 | import ConsoleExtend from './extend/console'; 5 | 6 | // a stub Hexo object 7 | // see `hexojs/hexo/lib/hexo/index.ts` 8 | 9 | type Callback = (err?: any, value?: any) => void; 10 | 11 | class Context { 12 | base_dir: string; 13 | log: ReturnType; 14 | extend: { 15 | console: ConsoleExtend; 16 | }; 17 | version?: string | null; 18 | 19 | constructor(base = process.cwd(), args = {}) { 20 | this.base_dir = base; 21 | this.log = logger(args); 22 | 23 | this.extend = { 24 | console: new ConsoleExtend() 25 | }; 26 | } 27 | 28 | init() { 29 | // Do nothing 30 | } 31 | 32 | call(name: string, args: object, callback?: Callback); 33 | call(name: string, callback?: Callback); 34 | call(name: string, args?: object | Callback, callback?: Callback) { 35 | if (!callback && typeof args === 'function') { 36 | callback = args as Callback; 37 | args = {}; 38 | } 39 | 40 | return new Promise((resolve, reject) => { 41 | const c = this.extend.console.get(name); 42 | 43 | if (c) { 44 | c.call(this, args).then(resolve, reject); 45 | } else { 46 | reject(new Error(`Console \`${name}\` has not been registered yet!`)); 47 | } 48 | }).asCallback(callback); 49 | } 50 | 51 | exit(err?: Error) { 52 | if (err) { 53 | this.log.fatal( 54 | {err}, 55 | 'Something\'s wrong. Maybe you can find the solution here: %s', 56 | underline('https://hexo.io/docs/troubleshooting.html') 57 | ); 58 | } 59 | 60 | return Promise.resolve(); 61 | } 62 | 63 | unwatch() { 64 | // Do nothing 65 | } 66 | } 67 | 68 | export = Context; 69 | -------------------------------------------------------------------------------- /lib/extend/console.ts: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import abbrev from 'abbrev'; 3 | import type { Options, Callback, Store, Alias } from '../types'; 4 | 5 | class Console { 6 | store: Store; 7 | alias: Alias; 8 | 9 | constructor() { 10 | this.store = {}; 11 | this.alias = {}; 12 | } 13 | 14 | get(name: string) { 15 | name = name.toLowerCase(); 16 | return this.store[this.alias[name]]; 17 | } 18 | 19 | list() { 20 | return this.store; 21 | } 22 | 23 | register(name: string, desc: string, options: Options, fn: Callback): void; 24 | register(name: string, options: Options, fn: Callback): void; 25 | register(name: string, desc: string, fn: Callback): void; 26 | register(name: string, fn: Callback): void; 27 | register(name: string, desc: string | Options | Callback, options?: Options | Callback, fn?: Callback) { 28 | if (!name) throw new TypeError('name is required'); 29 | 30 | if (!fn) { 31 | if (options) { 32 | if (typeof options === 'function') { 33 | fn = options as Callback; 34 | 35 | if (typeof desc === 'object') { // name, options, fn 36 | options = desc; 37 | desc = ''; 38 | } else { // name, desc, fn 39 | options = {}; 40 | } 41 | } else { 42 | throw new TypeError('fn must be a function'); 43 | } 44 | } else { 45 | // name, fn 46 | if (typeof desc === 'function') { 47 | fn = desc as Callback; 48 | options = {}; 49 | desc = ''; 50 | } else { 51 | throw new TypeError('fn must be a function'); 52 | } 53 | } 54 | } 55 | 56 | if (fn.length > 1) { 57 | fn = Promise.promisify(fn); 58 | } else { 59 | fn = Promise.method(fn); 60 | } 61 | 62 | this.store[name.toLowerCase()] = fn; 63 | const c = fn; 64 | c.options = options as Options; 65 | c.desc = desc as string; 66 | 67 | this.alias = abbrev(Object.keys(this.store)); 68 | } 69 | } 70 | 71 | export = Console; 72 | -------------------------------------------------------------------------------- /lib/find_pkg.ts: -------------------------------------------------------------------------------- 1 | import { resolve, join, dirname } from 'path'; 2 | import { readFile } from 'hexo-fs'; 3 | 4 | interface findPkgArgs { 5 | cwd?: string; 6 | } 7 | 8 | function findPkg(cwd: string, args: findPkgArgs = {}) { 9 | if (args.cwd) { 10 | cwd = resolve(cwd, args.cwd); 11 | } 12 | 13 | return checkPkg(cwd); 14 | } 15 | 16 | function checkPkg(path: string) { 17 | const pkgPath = join(path, 'package.json'); 18 | 19 | return readFile(pkgPath).then(content => { 20 | const json = JSON.parse(content as string); 21 | if (typeof json.hexo === 'object') return path; 22 | }).catch(err => { 23 | if (err && err.code === 'ENOENT') { 24 | const parent = dirname(path); 25 | 26 | if (parent === path) return; 27 | return checkPkg(parent); 28 | } 29 | 30 | throw err; 31 | }); 32 | } 33 | 34 | export = findPkg; 35 | -------------------------------------------------------------------------------- /lib/goodbye.ts: -------------------------------------------------------------------------------- 1 | const byeWords = [ 2 | 'Good bye', 3 | 'See you again', 4 | 'Farewell', 5 | 'Have a nice day', 6 | 'Bye!', 7 | 'Catch you later' 8 | ]; 9 | 10 | export = () => byeWords[(Math.random() * byeWords.length) | 0]; 11 | -------------------------------------------------------------------------------- /lib/hexo.ts: -------------------------------------------------------------------------------- 1 | import { magenta } from 'picocolors'; 2 | import tildify from 'tildify'; 3 | import Promise from 'bluebird'; 4 | import Context from './context'; 5 | import findPkg from './find_pkg'; 6 | import goodbye from './goodbye'; 7 | import minimist from 'minimist'; 8 | import resolve from 'resolve'; 9 | import { camelCaseKeys } from 'hexo-util'; 10 | import registerConsole from './console'; 11 | import helpConsole from './console/help'; 12 | import initConsole from './console/init'; 13 | import versionConsole from './console/version'; 14 | 15 | class HexoNotFoundError extends Error {} 16 | 17 | function entry(cwd = process.cwd(), args) { 18 | args = camelCaseKeys(args || minimist(process.argv.slice(2), { string: ['_', 'p', 'path', 's', 'slug'] })); 19 | 20 | let hexo = new Context(cwd, args); 21 | let { log } = hexo; 22 | 23 | // Change the title in console 24 | process.title = 'hexo'; 25 | 26 | function handleError(err) { 27 | if (err && !(err instanceof HexoNotFoundError)) { 28 | log.fatal(err); 29 | } 30 | 31 | process.exitCode = 2; 32 | } 33 | 34 | return findPkg(cwd, args).then(path => { 35 | if (!path) return; 36 | 37 | hexo.base_dir = path; 38 | 39 | return loadModule(path, args).catch(err => { 40 | log.error(err.message); 41 | log.error('Local hexo loading failed in %s', magenta(tildify(path))); 42 | log.error('Try running: \'rm -rf node_modules && npm install --force\''); 43 | throw new HexoNotFoundError(); 44 | }); 45 | }).then(mod => { 46 | if (mod) hexo = mod; 47 | log = hexo.log; 48 | 49 | registerConsole(hexo); 50 | 51 | return hexo.init(); 52 | }).then(() => { 53 | let cmd = 'help'; 54 | 55 | if (!args.h && !args.help) { 56 | const c = args._.shift(); 57 | if (c && hexo.extend.console.get(c)) cmd = c; 58 | } 59 | 60 | watchSignal(hexo); 61 | 62 | return hexo.call(cmd, args).then(() => hexo.exit()).catch(err => hexo.exit(err).then(() => { 63 | // `hexo.exit()` already dumped `err` 64 | handleError(null); 65 | })); 66 | }).catch(handleError); 67 | } 68 | 69 | entry.console = { 70 | init: initConsole, 71 | help: helpConsole, 72 | version: versionConsole 73 | }; 74 | 75 | entry.version = require('../package.json').version as string; 76 | 77 | function loadModule(path, args) { 78 | return Promise.try(() => { 79 | const modulePath = resolve.sync('hexo', { basedir: path }); 80 | const Hexo = require(modulePath); 81 | 82 | return new Hexo(path, args); 83 | }); 84 | } 85 | 86 | function watchSignal(hexo: Context) { 87 | process.on('SIGINT', () => { 88 | hexo.log.info(goodbye()); 89 | hexo.unwatch(); 90 | 91 | hexo.exit().then(() => { 92 | // eslint-disable-next-line no-process-exit 93 | process.exit(); 94 | }); 95 | }); 96 | } 97 | 98 | export = entry; 99 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface Command { 2 | name: string; 3 | desc: string; 4 | description?: string; 5 | } 6 | 7 | export interface Options { 8 | desc?: string; 9 | description?: string; 10 | usage?: string; 11 | arguments?: Command[]; 12 | options?: Command[]; 13 | commands?: Command[]; 14 | } 15 | 16 | export interface Callback { 17 | (args?: object): any; 18 | options?: Options; 19 | desc?: string; 20 | description?: string; 21 | } 22 | 23 | export interface Store { 24 | [key: string]: Callback; 25 | } 26 | 27 | export interface Alias { 28 | [key: string]: string; 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-cli", 3 | "version": "4.3.2", 4 | "description": "Command line interface for Hexo", 5 | "main": "dist/hexo", 6 | "bin": { 7 | "hexo": "./bin/hexo" 8 | }, 9 | "files": [ 10 | "dist/**", 11 | "completion", 12 | "bin", 13 | "assets" 14 | ], 15 | "types": "./dist/hexo.d.ts", 16 | "scripts": { 17 | "prepublishOnly": "npm install && npm run clean && npm run build", 18 | "build": "tsc -b", 19 | "clean": "tsc -b --clean", 20 | "eslint": "eslint .", 21 | "pretest": "npm run clean && npm run build", 22 | "test": "mocha test/**/*.ts --require ts-node/register", 23 | "test-cov": "nyc --reporter=lcovonly npm test", 24 | "prepare": "git submodule init && git submodule update && git submodule foreach git pull origin master" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/hexojs/hexo-cli.git" 29 | }, 30 | "homepage": "https://hexo.io/", 31 | "keywords": [ 32 | "website", 33 | "blog", 34 | "cms", 35 | "framework", 36 | "hexo", 37 | "cli" 38 | ], 39 | "author": "Tommy Chen (https://zespia.tw)", 40 | "maintainers": [ 41 | "Abner Chou (https://abnerchou.me)" 42 | ], 43 | "license": "MIT", 44 | "dependencies": { 45 | "abbrev": "^2.0.0", 46 | "bluebird": "^3.7.2", 47 | "command-exists": "^1.2.9", 48 | "hexo-fs": "^4.1.1", 49 | "hexo-log": "^4.0.1", 50 | "hexo-util": "^3.3.0", 51 | "minimist": "^1.2.5", 52 | "picocolors": "^1.0.0", 53 | "resolve": "^1.20.0", 54 | "tildify": "^2.0.0" 55 | }, 56 | "devDependencies": { 57 | "@types/abbrev": "^1.1.5", 58 | "@types/bluebird": "^3.5.37", 59 | "@types/chai": "^4.3.14", 60 | "@types/command-exists": "^1.2.3", 61 | "@types/minimist": "^1.2.5", 62 | "@types/mocha": "^10.0.6", 63 | "@types/node": "^18.11.8", 64 | "@types/rewire": "^2.5.30", 65 | "@types/sinon": "^17.0.3", 66 | "chai": "^4.3.4", 67 | "eslint": "^8.2.0", 68 | "eslint-config-hexo": "^5.0.0", 69 | "hexo-renderer-marked": "^6.0.0", 70 | "mocha": "^10.0.0", 71 | "nyc": "^15.1.0", 72 | "rewire": "^6.0.0", 73 | "sinon": "^17.0.1", 74 | "ts-node": "^10.9.1", 75 | "typescript": "^5.0.0" 76 | }, 77 | "engines": { 78 | "node": ">=14" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "hexo/ts-test", 3 | "rules": { 4 | "@typescript-eslint/no-var-requires": 0, 5 | "@typescript-eslint/ban-ts-comment": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/scripts/console.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinon from 'sinon'; 3 | import Console from '../../lib/extend/console'; 4 | chai.should(); 5 | 6 | describe('console', () => { 7 | it('register - name required', () => { 8 | const consoleExtend = new Console(); 9 | try { 10 | // @ts-expect-error 11 | consoleExtend.register(); 12 | } catch (err) { 13 | err.should.have.property('message', 'name is required'); 14 | } 15 | }); 16 | 17 | it('register - name, invalid fn', () => { 18 | const consoleExtend = new Console(); 19 | try { 20 | // @ts-expect-error 21 | consoleExtend.register('test', 'fn'); 22 | } catch (err) { 23 | err.should.have.property('message', 'fn must be a function'); 24 | } 25 | }); 26 | 27 | it('register - name, desc, invalid fn', () => { 28 | const consoleExtend = new Console(); 29 | try { 30 | // @ts-expect-error 31 | consoleExtend.register('test', 'desc', 'fn'); 32 | } catch (err) { 33 | err.should.have.property('message', 'fn must be a function'); 34 | } 35 | }); 36 | 37 | it('register - name, options, fn', () => { 38 | const consoleExtend = new Console(); 39 | const options = {}; 40 | const fn = sinon.spy(); 41 | consoleExtend.register('test', options, fn); 42 | const storeFn = consoleExtend.get('test'); 43 | storeFn(); 44 | fn.calledOnce.should.be.true; 45 | storeFn.options?.should.eql(options); 46 | storeFn.desc?.should.eql(''); 47 | }); 48 | 49 | it('register - name, desc, fn', () => { 50 | const consoleExtend = new Console(); 51 | const desc = 'desc'; 52 | const fn = sinon.spy(); 53 | consoleExtend.register('test', desc, fn); 54 | const storeFn = consoleExtend.get('test'); 55 | storeFn(); 56 | fn.calledOnce.should.be.true; 57 | storeFn.options?.should.deep.equal({}); 58 | storeFn.desc?.should.eql(desc); 59 | }); 60 | 61 | it('register - name, fn', () => { 62 | const consoleExtend = new Console(); 63 | const fn = sinon.spy(); 64 | consoleExtend.register('test', fn); 65 | const storeFn = consoleExtend.get('test'); 66 | storeFn(); 67 | fn.calledOnce.should.be.true; 68 | storeFn.options?.should.deep.equal({}); 69 | storeFn.desc?.should.eql(''); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/scripts/context.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinon from 'sinon'; 3 | import Context from '../../lib/context'; 4 | const should = chai.should(); 5 | 6 | describe('context', () => { 7 | describe('call', () => { 8 | const hexo = new Context(); 9 | const spy = sinon.spy(args => args); 10 | 11 | hexo.extend.console.register('test', spy); 12 | 13 | it('success', async () => { 14 | const args = {foo: 'bar'}; 15 | const result = await hexo.call('test', args); 16 | 17 | result.should.eql(args); 18 | spy.calledOnce.should.be.true; 19 | spy.lastCall.args[0].should.eql(args); 20 | spy.resetHistory(); 21 | }); 22 | 23 | it('console not registered', async () => { 24 | const hexo = new Context(); 25 | 26 | try { 27 | await hexo.call('wtf'); 28 | should.fail(); 29 | } catch (err) { 30 | err.should.have.property('message', 'Console `wtf` has not been registered yet!'); 31 | } 32 | }); 33 | 34 | it('with callback', done => { 35 | const args = {foo: 'bar'}; 36 | 37 | hexo.call('test', args, (err, result) => { 38 | if (err) return done(err); 39 | 40 | result.should.eql(args); 41 | spy.calledOnce.should.be.true; 42 | spy.lastCall.args[0].should.eql(args); 43 | spy.resetHistory(); 44 | done(); 45 | }); 46 | }); 47 | 48 | it('with callback but no args', done => { 49 | hexo.call('test', err => { 50 | if (err) return done(err); 51 | 52 | spy.calledOnce.should.be.true; 53 | spy.resetHistory(); 54 | done(); 55 | }); 56 | }); 57 | }); 58 | 59 | describe('exit', () => { 60 | let hexo, fatal; 61 | 62 | beforeEach(() => { 63 | hexo = new Context(); 64 | hexo.log.fatal = sinon.spy(); 65 | fatal = hexo.log.fatal; 66 | }); 67 | 68 | it('no error', () => { 69 | hexo.exit(); 70 | fatal.called.should.be.false; 71 | }); 72 | 73 | it('with error', () => { 74 | hexo.exit(new Error('error test')); 75 | fatal.calledOnce.should.be.true; 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/scripts/find_pkg.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import { rmdir, writeFile, unlink } from 'hexo-fs'; 3 | import { dirname, join } from 'path'; 4 | import findPkg from '../../lib/find_pkg'; 5 | const should = chai.should(); 6 | 7 | describe('Find package', () => { 8 | const baseDir = join(__dirname, 'find_pkg_test'); 9 | 10 | after(async () => await rmdir(baseDir)); 11 | 12 | it('not found', async () => { 13 | const path = await findPkg(baseDir, {}); 14 | should.not.exist(path); 15 | }); 16 | 17 | it('found', async () => { 18 | const pkgPath = join(baseDir, 'package.json'); 19 | 20 | await writeFile(pkgPath, '{"hexo": {}}'); 21 | const path = await findPkg(baseDir, {}); 22 | path.should.eql(baseDir); 23 | 24 | await unlink(pkgPath); 25 | }); 26 | 27 | it('found in parent directory', async () => { 28 | const pkgPath = join(baseDir, '../package.json'); 29 | 30 | await writeFile(pkgPath, '{"hexo": {}}'); 31 | const path = await findPkg(baseDir, {}); 32 | path.should.eql(dirname(pkgPath)); 33 | 34 | await unlink(pkgPath); 35 | }); 36 | 37 | it('found but don\'t have hexo data', async () => { 38 | const pkgPath = join(baseDir, 'package.json'); 39 | 40 | await writeFile(pkgPath, '{"name": "hexo"}'); 41 | const path = await findPkg(baseDir, {}); 42 | should.not.exist(path); 43 | 44 | await unlink(pkgPath); 45 | }); 46 | 47 | it('relative cwd', async () => { 48 | const pkgPath = join(baseDir, 'test', 'package.json'); 49 | 50 | await writeFile(pkgPath, '{"hexo": {}}'); 51 | const path = await findPkg(baseDir, { cwd: 'test' }); 52 | path.should.eql(dirname(pkgPath)); 53 | 54 | await unlink(pkgPath); 55 | }); 56 | 57 | it('specify cwd but don\'t have hexo data', async () => { 58 | const path = await findPkg(baseDir, {cwd: 'test'}); 59 | should.not.exist(path); 60 | }); 61 | 62 | it('absolute cwd', async () => { 63 | const pkgPath = join(baseDir, 'test', 'package.json'); 64 | const cwd = dirname(pkgPath); 65 | 66 | await writeFile(pkgPath, '{"hexo": {}}'); 67 | const path = await findPkg(baseDir, { cwd }); 68 | path.should.eql(cwd); 69 | 70 | await unlink(pkgPath); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/scripts/help.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinon, { type SinonStub } from 'sinon'; 3 | import { readFile } from 'hexo-fs'; 4 | import { join } from 'path'; 5 | import { format } from 'util'; 6 | import rewire from 'rewire'; 7 | import Context from '../../lib/context'; 8 | import console from '../../lib/console'; 9 | chai.should(); 10 | 11 | function getConsoleLog({ args }) { 12 | return args.map(arr => format(...arr)).join('\n'); 13 | } 14 | 15 | describe('help', () => { 16 | const helpModule = rewire('../../dist/console/help'); 17 | const hexo = new Context(); 18 | 19 | console(hexo); 20 | 21 | it('show global help', () => { 22 | const spy = sinon.spy(); 23 | 24 | return helpModule.__with__({ 25 | console: { 26 | log: spy 27 | } 28 | })(async () => { 29 | await helpModule.call(hexo, {_: []}); 30 | const output = getConsoleLog(spy); 31 | 32 | output.should.contain('Usage: hexo '); 33 | }); 34 | }); 35 | 36 | it('show help on a command', () => { 37 | const spy = sinon.spy(); 38 | 39 | return helpModule.__with__({ 40 | console: { 41 | log: spy 42 | } 43 | })(async () => { 44 | await helpModule.call(hexo, {_: ['init']}); 45 | const output = getConsoleLog(spy); 46 | 47 | output.should.contain('Usage: hexo init'); 48 | }); 49 | }); 50 | 51 | it('show help on a command with alias', () => { 52 | const spy = sinon.spy(); 53 | 54 | return helpModule.__with__({ 55 | console: { 56 | log: spy 57 | } 58 | })(async () => { 59 | await helpModule.call(hexo, {_: ['i']}); 60 | const output = getConsoleLog(spy); 61 | 62 | output.should.contain('Usage: hexo init'); 63 | }); 64 | }); 65 | 66 | it('show command description', () => { 67 | const spy = sinon.spy(); 68 | 69 | return helpModule.__with__({ 70 | console: { 71 | log: spy 72 | } 73 | })(async () => { 74 | await helpModule.call(hexo, {_: ['init']}); 75 | const output = getConsoleLog(spy); 76 | 77 | output.should.contain(`Description:\n${hexo.extend.console.get('init').options!.desc}`); 78 | }); 79 | }); 80 | 81 | it('show command usage', () => { 82 | const spy = sinon.spy(); 83 | 84 | return helpModule.__with__({ 85 | console: { 86 | log: spy 87 | } 88 | })(async () => { 89 | await helpModule.call(hexo, {_: ['init']}); 90 | const output = getConsoleLog(spy); 91 | 92 | output.should.contain(`Usage: hexo init ${hexo.extend.console.get('init').options!.usage}`); 93 | }); 94 | }); 95 | 96 | it('show command arguments', () => { 97 | const spy = sinon.spy(); 98 | 99 | return helpModule.__with__({ 100 | console: { 101 | log: spy 102 | } 103 | })(async () => { 104 | await helpModule.call(hexo, {_: ['init']}); 105 | const output = getConsoleLog(spy); 106 | 107 | hexo.extend.console.get('init').options!.arguments!.forEach(arg => { 108 | output.should.contain(arg.name); 109 | output.should.contain(arg.desc); 110 | }); 111 | }); 112 | }); 113 | 114 | it('show command options', () => { 115 | const spy = sinon.spy(); 116 | 117 | return helpModule.__with__({ 118 | console: { 119 | log: spy 120 | } 121 | })(async () => { 122 | await helpModule.call(hexo, {_: ['init']}); 123 | const output = getConsoleLog(spy); 124 | 125 | hexo.extend.console.get('init').options!.options!.forEach(option => { 126 | output.should.contain(option.name); 127 | output.should.contain(option.desc); 128 | }); 129 | }); 130 | }); 131 | 132 | it('show version info', async () => { 133 | sinon.stub(hexo, 'call').callsFake(() => Promise.resolve()); 134 | 135 | await helpModule.call(hexo, {_: [], version: true}); 136 | 137 | const call = hexo.call as SinonStub; 138 | 139 | call.calledWith('version').should.be.true; 140 | 141 | await call.restore(); 142 | }); 143 | 144 | it('show console list', () => { 145 | const spy = sinon.spy(); 146 | 147 | return helpModule.__with__({ 148 | console: { 149 | log: spy 150 | } 151 | })(async () => { 152 | await helpModule.call(hexo, {_: [], consoleList: true}); 153 | const output = getConsoleLog(spy); 154 | 155 | output.should.eql(Object.keys(hexo.extend.console.list()).join('\n')); 156 | }); 157 | }); 158 | 159 | it('show completion script', () => { 160 | const spy = sinon.spy(); 161 | 162 | return helpModule.__with__({ 163 | console: { 164 | log: spy 165 | } 166 | })(async () => { 167 | await helpModule.call(hexo, {_: [], completion: 'bash'}); 168 | const output = getConsoleLog(spy); 169 | 170 | const script = await readFile(join(__dirname, '../../completion/bash')); 171 | script.should.eql(output); 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/scripts/hexo.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinon from 'sinon'; 3 | import rewire from 'rewire'; 4 | import ConsoleExtend from '../../lib/extend/console'; 5 | chai.should(); 6 | 7 | require('chai').should(); 8 | 9 | describe('hexo', () => { 10 | const cwd = process.cwd(); 11 | 12 | it('run help if no specified command', async () => { 13 | const spy = sinon.spy(); 14 | const hexo = rewire('../../dist/hexo'); 15 | return hexo.__with__({ 16 | console_1: { 17 | default: ctx => { 18 | ctx.extend.console.register('help', spy); 19 | } 20 | } 21 | })(async () => { 22 | // @ts-expect-error 23 | await hexo(cwd, {_: []}); 24 | spy.calledOnce.should.be.true; 25 | }); 26 | }); 27 | 28 | it('run specified command', async () => { 29 | const spy = sinon.spy(); 30 | const hexo = rewire('../../dist/hexo'); 31 | return hexo.__with__({ 32 | console_1: { 33 | default: ctx => { 34 | ctx.extend.console.register('test', spy); 35 | } 36 | } 37 | })(async () => { 38 | // @ts-expect-error 39 | await hexo(cwd, {_: ['test']}); 40 | spy.calledOnce.should.be.true; 41 | }); 42 | }); 43 | 44 | it('run help if specified command not found', async () => { 45 | const spy = sinon.spy(); 46 | const hexo = rewire('../../dist/hexo'); 47 | return hexo.__with__({ 48 | console_1: { 49 | default: ctx => { 50 | ctx.extend.console.register('help', spy); 51 | } 52 | } 53 | })(async () => { 54 | // @ts-expect-error 55 | await hexo(cwd, {_: ['test']}); 56 | spy.calledOnce.should.be.true; 57 | }); 58 | }); 59 | 60 | it('path - number (issue hexo#4334)', async () => { 61 | let args; 62 | const hexo = rewire('../../dist/hexo'); 63 | return hexo.__with__({ 64 | find_pkg_1: { 65 | default: (_cwd, _args) => { 66 | args = _args; 67 | return Promise.resolve(); 68 | } 69 | } 70 | })(async () => { 71 | process.argv = ['hexo', 'new', '--path', '123', 'test']; 72 | // @ts-expect-error 73 | hexo(null, null); 74 | args.path.should.eql('123'); 75 | process.argv = []; 76 | }); 77 | }); 78 | 79 | it('p - number (issue hexo#4334)', async () => { 80 | let args; 81 | const hexo = rewire('../../dist/hexo'); 82 | return hexo.__with__({ 83 | find_pkg_1: { 84 | default: (_cwd, _args) => { 85 | args = _args; 86 | return Promise.resolve(); 87 | } 88 | } 89 | })(async () => { 90 | process.argv = ['hexo', 'new', '-p', '123', 'test']; 91 | // @ts-expect-error 92 | hexo(null, null); 93 | args.p.should.eql('123'); 94 | process.argv = []; 95 | }); 96 | }); 97 | 98 | it('slug - number (issue hexo#4334)', async () => { 99 | let args; 100 | const hexo = rewire('../../dist/hexo'); 101 | return hexo.__with__({ 102 | find_pkg_1: { 103 | default: (_cwd, _args) => { 104 | args = _args; 105 | return Promise.resolve(); 106 | } 107 | } 108 | })(async () => { 109 | process.argv = ['hexo', 'new', '--slug', '123', 'test']; 110 | // @ts-expect-error 111 | hexo(null, null); 112 | args.slug.should.eql('123'); 113 | process.argv = []; 114 | }); 115 | }); 116 | 117 | it('s - number (issue hexo#4334)', async () => { 118 | let args; 119 | const hexo = rewire('../../dist/hexo'); 120 | return hexo.__with__({ 121 | find_pkg_1: { 122 | default: (_cwd, _args) => { 123 | args = _args; 124 | return Promise.resolve(); 125 | } 126 | } 127 | })(async () => { 128 | process.argv = ['hexo', 'new', '-s', '123', 'test']; 129 | // @ts-expect-error 130 | hexo(null, null); 131 | args.s.should.eql('123'); 132 | process.argv = []; 133 | }); 134 | }); 135 | 136 | it('should call init() method'); 137 | 138 | it('should handle HexoNotFoundError properly', () => { 139 | const spy = sinon.spy(); 140 | const hexo = rewire('../../dist/hexo'); 141 | const dummyPath = 'dummy'; 142 | const dummyError = 'test'; 143 | return hexo.__with__({ 144 | find_pkg_1: { 145 | default: () => Promise.resolve(dummyPath) 146 | }, 147 | loadModule: () => Promise.reject(new Error(dummyError)), 148 | context_1: { 149 | default: class Context { 150 | log: { error: typeof spy }; 151 | constructor() { 152 | this.log = { 153 | error: spy 154 | }; 155 | } 156 | } 157 | } 158 | })(async () => { 159 | // @ts-expect-error 160 | await hexo(cwd, {_: ['test']}); 161 | spy.args[0][0].should.eql(dummyError); 162 | spy.args[1][0].should.eql('Local hexo loading failed in %s'); 163 | spy.args[1][1].should.eql(`\x1B[35m${dummyPath}\x1B[39m`); 164 | spy.args[2][0].should.eql('Try running: \'rm -rf node_modules && npm install --force\''); 165 | process.exitCode?.should.eql(2); 166 | }); 167 | }); 168 | 169 | it('should handle other Error properly', () => { 170 | const spy = sinon.spy(); 171 | const hexo = rewire('../../dist/hexo'); 172 | const dummyPath = 'dummy'; 173 | const dummyError = 'error'; 174 | return hexo.__with__({ 175 | find_pkg_1: { 176 | default: () => Promise.resolve(dummyPath) 177 | }, 178 | loadModule: () => Promise.resolve(), 179 | console_1: { 180 | default: () => { /* empty */ } 181 | }, 182 | context_1: { 183 | default: class Context { 184 | log: { error: typeof spy, fatal: typeof spy }; 185 | constructor() { 186 | this.log = { 187 | error: spy, 188 | fatal: spy 189 | }; 190 | } 191 | init() { 192 | throw new Error(dummyError); 193 | } 194 | } 195 | } 196 | })(async () => { 197 | // @ts-expect-error 198 | await hexo(cwd, {_: ['test']}); 199 | spy.args[0][0].message.should.eql(dummyError); 200 | process.exitCode?.should.eql(2); 201 | }); 202 | }); 203 | 204 | it('should watch SIGINT signal', () => { 205 | const spy = sinon.spy(); 206 | const watchSpy = sinon.spy(); 207 | const exitSpy = sinon.spy(); 208 | const dummyPath = 'dummy'; 209 | const hexo = rewire('../../dist/hexo'); 210 | const processSpy = { 211 | on: process.on, 212 | emit: process.emit, 213 | exit: exitSpy 214 | }; 215 | return hexo.__with__({ 216 | find_pkg_1: { 217 | default: () => Promise.resolve(dummyPath) 218 | }, 219 | loadModule: () => Promise.resolve(), 220 | console_1: { 221 | default: () => { /* empty */ } 222 | }, 223 | process: processSpy, 224 | context_1: { 225 | default: class Context { 226 | log: { error: typeof spy, fatal: typeof spy, info: typeof spy }; 227 | extend: { 228 | console: ConsoleExtend; 229 | }; 230 | constructor() { 231 | this.log = { 232 | error: spy, 233 | fatal: spy, 234 | info: spy 235 | }; 236 | this.extend = { 237 | console: new ConsoleExtend() 238 | }; 239 | } 240 | init() { 241 | return Promise.resolve(); 242 | } 243 | call() { 244 | return Promise.resolve(processSpy.emit('SIGINT')); 245 | } 246 | unwatch() { 247 | watchSpy(); 248 | } 249 | exit() { 250 | return Promise.resolve(); 251 | } 252 | } 253 | } 254 | })(async () => { 255 | // @ts-expect-error 256 | await hexo(cwd, {_: ['help']}); 257 | [ 258 | 'Good bye', 259 | 'See you again', 260 | 'Farewell', 261 | 'Have a nice day', 262 | 'Bye!', 263 | 'Catch you later' 264 | ].includes(spy.args[0][0]).should.be.true; 265 | watchSpy.calledOnce.should.be.true; 266 | exitSpy.calledOnce.should.be.true; 267 | }); 268 | }); 269 | 270 | it('load hexo module in parent folder recursively'); 271 | }); 272 | -------------------------------------------------------------------------------- /test/scripts/init.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import { join } from 'path'; 3 | import { listDir, rmdir, createReadStream } from 'hexo-fs'; 4 | import { createSha1Hash } from 'hexo-util'; 5 | import rewire from 'rewire'; 6 | import Context from '../../lib/context'; 7 | chai.should(); 8 | 9 | const assetDir = join(__dirname, '../../assets'); 10 | 11 | describe('init', () => { 12 | const baseDir = join(__dirname, 'init_test'); 13 | const initModule = rewire('../../dist/console/init'); 14 | const hexo = new Context(baseDir, { silent: true }); 15 | const init = initModule.bind(hexo); 16 | let assets: string[] = []; 17 | 18 | async function rmDir(path: string) { 19 | try { 20 | await rmdir(path); 21 | } catch (err) { 22 | if (err && err.code === 'ENOENT') return; 23 | throw err; 24 | } 25 | } 26 | 27 | function pipeStream(rs, ws) { 28 | return new Promise((resolve, reject) => { 29 | rs.pipe(ws) 30 | .on('error', reject) 31 | .on('finish', resolve); 32 | }); 33 | } 34 | 35 | async function compareFile(a, b) { 36 | const streamA = createSha1Hash(); 37 | const streamB = createSha1Hash(); 38 | 39 | await Promise.all([ 40 | pipeStream(createReadStream(a), streamA), 41 | pipeStream(createReadStream(b), streamB) 42 | ]); 43 | 44 | return streamA.read().equals(streamB.read()); 45 | } 46 | 47 | async function check(path) { 48 | for (const item of assets) { 49 | const result = await compareFile( 50 | join(assetDir, item), 51 | join(path, item) 52 | ); 53 | 54 | result.should.be.true; 55 | } 56 | 57 | } 58 | 59 | function withoutSpawn(fn) { 60 | return initModule.__with__({ 61 | 'spawn_1': () => Promise.reject(new Error('spawn is not available')) 62 | })(fn); 63 | } 64 | 65 | before(async () => { 66 | const files = await listDir(assetDir); 67 | assets = files; 68 | }); 69 | 70 | after(async () => await rmDir(baseDir)); 71 | 72 | it('current path', () => withoutSpawn(async () => { 73 | await init({_: []}); 74 | await check(baseDir); 75 | })); 76 | 77 | it('relative path', () => withoutSpawn(async () => { 78 | await init({_: ['test']}); 79 | await check(join(baseDir, 'test')); 80 | })); 81 | 82 | it('unconventional path - 0x', () => withoutSpawn(async () => { 83 | await init({_: ['0x400']}); 84 | await check(join(baseDir, '0x400')); 85 | })); 86 | 87 | it('unconventional path - 0b', () => withoutSpawn(async () => { 88 | await init({_: ['0b101']}); 89 | await check(join(baseDir, '0b101')); 90 | })); 91 | 92 | it('unconventional path - 0o', () => withoutSpawn(async () => { 93 | await init({_: ['0o71']}); 94 | await check(join(baseDir, '0o71')); 95 | })); 96 | 97 | it('unconventional path - undefined', () => withoutSpawn(async () => { 98 | await init({_: ['undefined']}); 99 | await check(join(baseDir, 'undefined')); 100 | })); 101 | 102 | it('unconventional path - null', () => withoutSpawn(async () => { 103 | await init({_: ['null']}); 104 | await check(join(baseDir, 'null')); 105 | })); 106 | 107 | it('unconventional path - true', () => withoutSpawn(async () => { 108 | await init({_: ['true']}); 109 | await check(join(baseDir, 'true')); 110 | })); 111 | 112 | it('path multi-charset', () => withoutSpawn(async () => { 113 | await init({_: ['中文']}); 114 | await check(join(baseDir, '中文')); 115 | })); 116 | 117 | it('absolute path', () => { 118 | const path = join(baseDir, 'test'); 119 | 120 | withoutSpawn(async () => { 121 | await init({_: [path]}); 122 | await check(path); 123 | }); 124 | }); 125 | 126 | it('git clone'); 127 | 128 | it('npm install'); 129 | }); 130 | -------------------------------------------------------------------------------- /test/scripts/version.ts: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinon from 'sinon'; 3 | import Context from '../../lib/context'; 4 | import { platform, release } from 'os'; 5 | import { format } from 'util'; 6 | import { version as cliVersion } from '../../package.json'; 7 | import rewire from 'rewire'; 8 | import { spawn } from 'hexo-util'; 9 | chai.should(); 10 | 11 | function getConsoleLog({ args }) { 12 | return args.map(arr => format(...arr)).join('\n'); 13 | } 14 | 15 | describe('version', () => { 16 | const versionModule = rewire('../../dist/console/version'); 17 | const hexo = new Context(); 18 | 19 | it('show version info', () => { 20 | const spy = sinon.spy(); 21 | 22 | return versionModule.__with__({ 23 | console: { 24 | log: spy 25 | } 26 | })(async () => { 27 | await versionModule.call(hexo, {_: []}); 28 | const output = getConsoleLog(spy); 29 | const expected: string[] = []; 30 | 31 | Object.keys(process.versions).forEach(key => { 32 | expected.push(`${key}: ${process.versions[key]}`); 33 | }); 34 | 35 | output.should.contain(`hexo-cli: ${cliVersion}`); 36 | output.should.contain(`os: ${platform()} ${release()}`); 37 | output.should.contain(expected.join('\n')); 38 | 39 | if (process.env.CI === 'true') { 40 | if (process.platform === 'darwin') { 41 | const osInfo = await spawn('sw_vers', '-productVersion'); 42 | output.should.contain(`os: ${platform()} ${release()} ${osInfo}`); 43 | } else if (process.platform === 'linux') { 44 | const v = await spawn('cat', '/etc/os-release') as string; 45 | const distro = (v || '').match(/NAME="(.+)"/); 46 | output.should.contain(`os: ${platform()} ${release()} ${distro![1]}`); 47 | } 48 | } 49 | }); 50 | }); 51 | 52 | it('show hexo version if available', () => { 53 | const spy = sinon.spy(); 54 | 55 | hexo.version = '3.2.1'; 56 | 57 | return versionModule.__with__({ 58 | console: { 59 | log: spy 60 | } 61 | })(async () => { 62 | await versionModule.call(hexo, {_: []}); 63 | const output = getConsoleLog(spy); 64 | 65 | output.should.contain(`hexo: ${hexo.version}`); 66 | }).finally(() => { 67 | hexo.version = null; 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "sourceMap": true, 6 | "outDir": "dist", 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | "types": [ 11 | "node", 12 | "mocha" 13 | ] 14 | }, 15 | "include": [ 16 | "lib/hexo.ts" 17 | ], 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } --------------------------------------------------------------------------------