├── .dockerignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .lc-completion.bash ├── .npmignore ├── .travis.yml ├── .vscode └── launch.json ├── Dockerfile ├── LICENSE ├── README.md ├── bin ├── entrypoint ├── install ├── leetcode ├── pkg ├── pkg.bat └── pkg.sh ├── colors ├── blue.json ├── dark.json ├── default.json ├── molokai.json ├── orange.json ├── pink.json ├── solarized.json └── solarized.light.json ├── docs ├── _config.yml ├── advanced.md ├── commands.md ├── demo.html ├── index.html ├── install.md ├── logo.png ├── releases.md ├── screenshots │ └── intro.2018.01.13.gif └── workflow.png ├── icons ├── ascii.json ├── default.json └── win7.json ├── lib ├── cache.js ├── chalk.js ├── cli.js ├── commands │ ├── cache.js │ ├── config.js │ ├── list.js │ ├── plugin.js │ ├── session.js │ ├── show.js │ ├── star.js │ ├── stat.js │ ├── submission.js │ ├── submit.js │ ├── test.js │ ├── user.js │ └── version.js ├── config.js ├── core.js ├── file.js ├── helper.js ├── icon.js ├── log.js ├── plugin.js ├── plugins │ ├── cache.js │ ├── company.js │ ├── leetcode.cn.js │ ├── leetcode.js │ ├── retry.js │ └── solution.discuss.js ├── queue.js ├── session.js └── sprintf.js ├── package-lock.json ├── package.json ├── templates ├── codeonly.tpl └── detailed.tpl └── test ├── helper.js ├── mock ├── add-two-numbers.20161015.json ├── favorites.json.20170716 ├── find-the-difference-star.json.20200821 ├── find-the-difference-unstar.json.20200821 ├── find-the-difference.json.20171216 ├── locked.html.20161015 ├── problems.json.20160911 ├── problems.nologin.json.20161015 ├── two-sum.submission.73790064.html.20161006 └── two-sum.submissions.json.20170425 ├── plugins ├── test_cache.js ├── test_leetcode.js └── test_retry.js ├── test_cache.js ├── test_chalk.js ├── test_config.js ├── test_core.js ├── test_file.js ├── test_helper.js ├── test_icon.js ├── test_log.js ├── test_plugin.js ├── test_queue.js ├── test_session.js └── test_sprintf.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .npm 4 | .nyc_output 5 | .DS_Store 6 | 7 | coverage 8 | dist 9 | node_modules 10 | npm-debug.log* 11 | tmp 12 | 13 | *.log 14 | *.swp 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": false, 4 | "es6": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "google", 10 | "eslint:recommended" 11 | ], 12 | "rules": { 13 | "block-spacing": [2, "always"], 14 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 15 | "camelcase": [2, {properties: "never"}], 16 | "comma-dangle": 0, 17 | "curly": 0, 18 | "key-spacing": [2, {align: "value"}], 19 | "max-len": [1, 120], 20 | "no-control-regex": 0, 21 | "no-console": 1, 22 | "no-empty": [2, { "allowEmptyCatch": true }], 23 | "no-eval": 1, // we use it on purpose 24 | "no-loop-func": 1, 25 | "no-multi-spaces": 0, 26 | "no-proto": 1, 27 | "no-unused-expressions": 1, 28 | "no-unused-vars": 1, 29 | "no-var": 0, 30 | "no-warning-comments": 0, 31 | "prefer-rest-params": 0, 32 | "prefer-spread": 0, 33 | "quote-props": 1, 34 | "quotes": [2, "single", {avoidEscape: true}], 35 | "require-jsdoc": 0, 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 17 | 18 | ### Problem Summary 19 | 20 | 21 | ### How to reproduce 22 | 27 | 28 | ### Environment 29 | 30 | 31 | - leetcode-cli version: 32 | 33 | - OS version: 34 | - Node version: 35 | - Npm version: 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | dist/ 40 | tmp/ 41 | *.swp 42 | .DS_Store 43 | -------------------------------------------------------------------------------- /.lc-completion.bash: -------------------------------------------------------------------------------- 1 | ###-begin-lc-completions-### 2 | # 3 | # yargs command completion script 4 | # 5 | # Installation: bin/leetcode completion >> ~/.bashrc 6 | # or bin/leetcode completion >> ~/.bash_profile on OSX. 7 | # 8 | _yargs_completions() 9 | { 10 | local cur_word args type_list 11 | 12 | cur_word="${COMP_WORDS[COMP_CWORD]}" 13 | args=("${COMP_WORDS[@]}") 14 | 15 | # ask yargs to generate completions. 16 | type_list=$(leetcode --get-yargs-completions "${args[@]}") 17 | 18 | COMPREPLY=( $(compgen -W "${type_list}" -- ${cur_word}) ) 19 | 20 | # if no match was found, fall back to filename completion 21 | if [ ${#COMPREPLY[@]} -eq 0 ]; then 22 | COMPREPLY=( $(compgen -f -- "${cur_word}" ) ) 23 | fi 24 | 25 | return 0 26 | } 27 | complete -F _yargs_completions leetcode 28 | ###-end-lc-completions-### 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | docs/ 3 | test/ 4 | .eslintrc.js 5 | .gitignore 6 | .travis.yml 7 | Dockerfile 8 | .dockerignore 9 | .nyc_output/ 10 | .vscode/ 11 | coverage/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | 5 | os: 6 | - linux 7 | - osx 8 | 9 | install: 10 | - npm install 11 | 12 | script: 13 | - npm test 14 | - if [ -n "$TRAVIS_TAG" ]; then npm run travis; fi 15 | 16 | deploy: 17 | provider: releases 18 | api_key: 19 | secure: "ayYe6HlYFrFposeIh2xX1DbdF3CRFnAHM5VvdtfVh/TtpcEvg4GRCanvzaSvsVajLjFZOZhGVgm+uZ1H6ba6jQuoOUvFJ667EVwQk7c8KDJrvZIMvzMxCgvSHb6N8VBh/5svWYa+7Kbd++3WP7XmkLpWli/DXvOSu6I6M7w+m/OI157mWPp0a7iy+Q+o1vSl/3INNIrd/vMT5F+ae1iBLFn3aHndtezhdQr+HrQCHaVP8OiK96rtjzaiRp+dyoMf4U71LoJGRpGZURv9imyXholoQutlT+bhRaumPqrqiwFRGMaL+xhfBZMySMND8wcO9rQnabiQf5Wo9J5aJMnixWjEIg9gGhJ8E96j9VwdUBA7yfHAbVhLrQ0h2TkZuUdqU1EnOWIbnPtjC9exv8R5X2WRs1fMz9j+XpNYclB4YdLclQ662nfsquccqfksDG1rS249WkSl1RIxr9fcD+60xYXgkG78wrTN8cr9NMGk5/AyMyHcvYjA+rGg1V8DZhzC3WZn9Q0NRJoc3b+xx9pxkaO7epBck5sAsNPO8b/bMGGKmgmR5tKSZUN+lTUKLI2znJcUC1dMKKpRCqr1To94ZYVe0G7SFbe+MH4guQXkd7sB6GnsR8/7g8OsVcAtV4DoEWfHwJQIE0bg/UzqubyBPSGPs1JBZm8nks/zTpOJ65o=" 20 | file: leetcode-cli.* 21 | file_glob: true 22 | skip_cleanup: true 23 | overwrite: true 24 | on: 25 | tags: true 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/bin/leetcode", 12 | "args": ["show", "1", "--solution"] 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Mocha Tests", 18 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 19 | "args": [ 20 | "-u", 21 | "tdd", 22 | "--timeout", 23 | "999999", 24 | "--colors", 25 | "${workspaceFolder}/test/plugins" 26 | ], 27 | "internalConsoleOptions": "openOnSessionStart" 28 | }, 29 | ] 30 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | LABEL maintainer="skygragon@gmail.com" 3 | 4 | WORKDIR /tmp/leetcode-cli 5 | COPY . . 6 | COPY bin/entrypoint / 7 | RUN npm install && \ 8 | tar zcf /leetcode-cli.tar.gz . && \ 9 | rm -rf /tmp/leetcode-cli 10 | 11 | WORKDIR /root 12 | VOLUME ["/root"] 13 | ENTRYPOINT ["/entrypoint"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 skygragon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://img.shields.io/npm/v/leetcode-cli.svg?style=flat)](https://www.npmjs.com/package/leetcode-cli) 2 | [![Releases](https://img.shields.io/github/release/skygragon/leetcode-cli.svg?style=flat)](https://github.com/skygragon/leetcode-cli/releases) 3 | [![license](https://img.shields.io/npm/l/leetcode-cli.svg?style=flat)](https://github.com/skygragon/leetcode-cli/blob/master/LICENSE) 4 | [![Build](https://img.shields.io/travis/skygragon/leetcode-cli.svg?style=flat)](https://travis-ci.org/skygragon/leetcode-cli) 5 | [![Join chat at https://gitter.im/skygragon/leetcode-cli](https://img.shields.io/gitter/room/skygragon/leetcode-cli.svg?style=flat)](https://gitter.im/skygragon/leetcode-cli) 6 | 7 | # leetcode-cli 8 | 9 | > Note: This repository is forked from [leetcode-cli](https://github.com/skygragon/leetcode-cli) for temporary usage. 10 | > Note: Copy cookie from webbrowser and Using **leetcode user -c** can temporary fix can't [login problem](https://github.com/jdneo/vscode-leetcode/issues/478). 11 | 12 | 13 | 14 | A productive cli tool to enjoy leetcode! 15 | 16 | Great thanks to leetcode.com, a really awesome website! 17 | 18 | ⦙ [Releases](https://skygragon.github.io/leetcode-cli/releases) ⦙ 19 | [Install](https://skygragon.github.io/leetcode-cli/install) ⦙ 20 | [Docs](https://skygragon.github.io/leetcode-cli/) ⦙ 21 | [Commands](https://skygragon.github.io/leetcode-cli/commands) ⦙ 22 | [Advanced](https://skygragon.github.io/leetcode-cli/advanced) ⦙ 23 | [Plugins](https://github.com/skygragon/leetcode-cli-plugins) ⦙ 24 | 25 | * A very [**EFFICIENT**](#quick-start) way to fight questions. 26 | * [**CACHING**](https://skygragon.github.io/leetcode-cli/advanced#cache) questions to ease offline thinking. 27 | * [**GENERATING**](https://skygragon.github.io/leetcode-cli/commands#show) source code before coding. 28 | * Live [**TEST**](https://skygragon.github.io/leetcode-cli/commands#test) and [**SUBMIT**](https://skygragon.github.io/leetcode-cli/commands#submit) with leetcode.com. 29 | * Download your previous [**SUBMISSION**](https://skygragon.github.io/leetcode-cli/commands#submission). 30 | * Trace your coding [**STATUS**](https://skygragon.github.io/leetcode-cli/commands#stat). 31 | * [**AUTO LOGIN**](https://skygragon.github.io/leetcode-cli/advanced#auto-login) among multiple agents with single account. 32 | * Multiple [**THEMES**](https://skygragon.github.io/leetcode-cli/advanced#color-themes) support. 33 | * More [**PLUGINS**](https://skygragon.github.io/leetcode-cli/advanced#plugins) to enjoy extra features! 34 | 35 | ## Screenshot 36 | 37 | 38 | 39 | ## Quick Start 40 | 41 | Read help first $ leetcode help 42 | Login with your leetcode account $ leetcode user -l 43 | Login with third party account--GitHub $ leetcode user -g 44 | Login with third party account--LinkedIn $ leetcode user -i 45 | Cookie login with cookie $ leetcode user -c 46 | Browse all questions $ leetcode list 47 | Choose one question $ leetcode show 1 -g -l cpp 48 | Coding it! 49 | Run test(s) and pray... $ leetcode test ./two-sum.cpp -t '[3,2,4]\n7' 50 | Submit final solution! $ leetcode submit ./two-sum.cpp 51 | -------------------------------------------------------------------------------- /bin/entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | srcdir=/root/leetcode-cli 4 | leetcode=$srcdir/bin/leetcode 5 | 6 | if [ ! -f "$leetcode" ]; then 7 | echo "Unpacking leetcode-cli code ..." 8 | mkdir -p $srcdir 9 | tar zxf /leetcode-cli.tar.gz -C $srcdir 10 | fi 11 | 12 | export TERM=xterm-256color 13 | exec $leetcode $@ 14 | -------------------------------------------------------------------------------- /bin/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ENVFILE=.env.json 4 | 5 | check() { 6 | printf "Checking $1 ... " 7 | $1 --version > /dev/null 2>&1 8 | if [ $? != 0 ]; then 9 | echo "No" 10 | echo "[ERROR] Missing $1!" 11 | exit 1; 12 | fi 13 | echo "Yes" 14 | } 15 | 16 | create() { 17 | cat << EOF > $1 18 | { 19 | "commit": { 20 | "full": "`git rev-parse HEAD`", 21 | "short": "`git rev-parse --short HEAD`" 22 | }, 23 | "node": "`node -v`", 24 | "npm": "`npm -v`" 25 | } 26 | EOF 27 | } 28 | 29 | check git 30 | check node 31 | check npm 32 | 33 | create $ENVFILE 34 | 35 | npm install -g . 36 | echo "leetcode-cli successfully installed." 37 | -------------------------------------------------------------------------------- /bin/leetcode: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli').run(); 4 | -------------------------------------------------------------------------------- /bin/pkg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const arch = require('os').arch(); 4 | var os = process.platform; 5 | const ver = process.versions.node.split('.')[0]; 6 | 7 | var bin = './bin/pkg.sh'; 8 | var args = [arch, os, ver]; 9 | 10 | if (os === 'darwin') { 11 | args[1] = 'macos'; 12 | } else if (os === 'win32') { 13 | bin = 'cmd.exe'; 14 | args = ['/c', 'bin\\pkg.bat'].concat(args); 15 | } 16 | 17 | var proc = require('child_process').spawn(bin, args); 18 | proc.stdout.on('data', x => console.log(x.toString().trimRight('\n'))); 19 | proc.stderr.on('data', x => console.log(x.toString().trimRight('\n'))); 20 | proc.on('close', process.exit); -------------------------------------------------------------------------------- /bin/pkg.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set arch=%1 3 | set os=%2 4 | set ver=%3 5 | 6 | set dist=dist\ 7 | set file=leetcode-cli.node%ver%.%os%.%arch%.zip 8 | 9 | mkdir %dist% 10 | del /q %dist%\* 11 | del /q *.zip 12 | 13 | for %%x in (company cookie.chrome cookie.firefox cpp.lint cpp.run github leetcode.cn lintcode solution.discuss) do ( 14 | echo [%%x] 15 | node bin\leetcode ext -i %%x 16 | if %ERRORLEVEL% gtr 0 exit /b 1 17 | ) 18 | 19 | for /r . %%x in (*.node) do copy %%x %dist% 20 | call npm run pkg -- node%ver%-%os%-%arch% 21 | if %ERRORLEVEL% gtr 0 exit /b 1 22 | 23 | 7z a %file% %dist% 24 | if %ERRORLEVEL% gtr 0 exit /b 1 25 | exit 0 -------------------------------------------------------------------------------- /bin/pkg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | arch=$1 4 | os=$2 5 | ver=$3 6 | 7 | DIST=./dist 8 | FILE=leetcode-cli.node$ver.$os.$arch.tar.gz 9 | 10 | mkdir -p $DIST 11 | rm -rf $DIST/* 12 | rm -rf $FILE 13 | 14 | plugins="company cookie.chrome cookie.firefox cpp.lint cpp.run github leetcode.cn lintcode solution.discuss" 15 | 16 | for plugin in $plugins; do 17 | echo "[$plugin]" 18 | ./bin/leetcode ext -i $plugin 19 | done 20 | 21 | find node_modules -name "*.node" -exec cp {} $DIST \; 22 | npm run pkg -- node$ver-$os-$arch 23 | 24 | tar zcvf $FILE $DIST 25 | ls -al $FILE 26 | exit 0 -------------------------------------------------------------------------------- /colors/blue.json: -------------------------------------------------------------------------------- 1 | { 2 | "blue": "#0000ff", 3 | "cyan": "#b0c4de", 4 | "gray": "#483d8b", 5 | "green": "#00bfff", 6 | "magenta": "#6a5acd", 7 | "red": "#ae81ff", 8 | "white": "#f0f8ff", 9 | "yellow": "#87cefa" 10 | } 11 | -------------------------------------------------------------------------------- /colors/dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "blue": "#000099", 3 | "cyan": "#009999", 4 | "gray": "#455354", 5 | "green": "#009900", 6 | "magenta": "#990099", 7 | "red": "#990000", 8 | "yellow": "#999900" 9 | } 10 | -------------------------------------------------------------------------------- /colors/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "black": "#000000", 3 | "blue": "#0000ff", 4 | "cyan": "#00ffff", 5 | "gray": "#999999", 6 | "green": "#00ff00", 7 | "magenta": "#ff00ff", 8 | "red": "#ff0000", 9 | "white": "#ffffff", 10 | "yellow": "#ffff00" 11 | } 12 | -------------------------------------------------------------------------------- /colors/molokai.json: -------------------------------------------------------------------------------- 1 | { 2 | "blue": "#66D9EF", 3 | "cyan": "#AE81FF", 4 | "gray": "#75715E", 5 | "green": "#87FF00", 6 | "magenta": "#FF46FF", 7 | "red": "#D7005F", 8 | "white": "#F8F8F2", 9 | "yellow": "#FD971F" 10 | } 11 | -------------------------------------------------------------------------------- /colors/orange.json: -------------------------------------------------------------------------------- 1 | { 2 | "blue": "#808000", 3 | "cyan": "#b8860b", 4 | "gray": "#deb887", 5 | "green": "#ffa500", 6 | "magenta": "#d2691e", 7 | "red": "#ff4500", 8 | "white": "#fdf5eb", 9 | "yellow": "#ffd700" 10 | } 11 | -------------------------------------------------------------------------------- /colors/pink.json: -------------------------------------------------------------------------------- 1 | { 2 | "blue": "#8a2be2", 3 | "cyan": "#800080", 4 | "gray": "#d8bfd8", 5 | "green": "#ff00ff", 6 | "magenta": "#db7093", 7 | "red": "#ff1493", 8 | "white": "#fff0f5", 9 | "yellow": "#ffc0cb" 10 | } 11 | -------------------------------------------------------------------------------- /colors/solarized.json: -------------------------------------------------------------------------------- 1 | { 2 | "black": "#073642", 3 | "blue": "#268bd2", 4 | "cyan": "#2aa198", 5 | "green": "#859900", 6 | "magenta": "#d33682", 7 | "red": "#dc322f", 8 | "white": "#eee8d5", 9 | "yellow": "#b58900" 10 | } 11 | -------------------------------------------------------------------------------- /colors/solarized.light.json: -------------------------------------------------------------------------------- 1 | { 2 | "black": "#262626", 3 | "blue": "#0087ff", 4 | "cyan": "#00afaf", 5 | "green": "#5f8700", 6 | "magenta": "#af005f", 7 | "red": "#d70000", 8 | "white": "#d7d7af", 9 | "yellow": "#af8700" 10 | } 11 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/advanced.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Advanced Topic 4 | --- 5 | 6 | * [Aliases](#aliases) 7 | * [Auto Login](#auto-login) 8 | * [Bash Completion](#bash-completion) 9 | * [Cache](#cache) 10 | * [Configuration](#configuration) 11 | * [Color Themes](#color-themes) 12 | * [File Name](#file-name) 13 | * [Log Levels](#log-levels) 14 | * [Plugins](#plugins) 15 | 16 | # Aliases 17 | 18 | The commands in leetcode-cli usually has builtin aliases as below: 19 | 20 | |Command |Aliases | 21 | |----------|-----------------------| 22 | |config |conf, cfg, setting | 23 | |list |ls | 24 | |plugin |extension, ext | 25 | |session |branch | 26 | |show |view, pick | 27 | |star |like, favorite | 28 | |stat |stats, progress, report| 29 | |submission|pull | 30 | |submit |push, commit | 31 | |test |run | 32 | |user |account | 33 | |version |info, env | 34 | 35 | # Auto Login 36 | 37 | Leetcode.com is restricting only one session alive in the same time, which means if you login same account otherwhere, the existing login session will be expired immediately. This will greatly harm your experience since you have to re-login again and again among different sessions. 38 | 39 | The good news is leetcode-cli will help a lot on this by trying re-login transparently and automatically without interrupting your current work whenever it detects your current session is expired. To enable this feature you could add following in your config then login again: 40 | 41 | { 42 | "autologin": { 43 | "enable": true 44 | } 45 | } 46 | 47 | **NOTE: once enabled, your PASSWORD will be persisted locally for further using, so PLEASE be careful to ONLY enable this on your OWN computer for the sake of security!** 48 | 49 | # Bash Completion 50 | 51 | Copy `.lc-completion.bash` to your home directory, and source it in .bashrc (Linux) or .bash_profile (MacOS). 52 | 53 | $ cp .lc-completion.bash ~ 54 | $ echo "source ~/.lc-completion.bash" >> ~/.bashrc 55 | $ source ~/.bashrc 56 | 57 | $ leetcode list -- 58 | --help --keyword --query --stat 59 | 60 | **NOTE: it might become slower in bash with this enabled, personally I would NOT suggest to use it...** 61 | 62 | # Cache 63 | 64 | The local cache folder (`.lc/`) is in your home directory, e.g. 65 | 66 | $ ls -a1 ~/.lc/ 67 | cache # folder of cached questions 68 | config.json # user customized config 69 | user.json # user account info 70 | 71 | $ ls -a1 ~/.lc/cache/ 72 | problems.json # cached questions list 73 | 1.two-sum.algorithms.json # cached specific question 74 | 75 | **NOTE: Normally you don't need dig into the folder to manipulate those files. Use [cache command](https://skygragon.github.io/leetcode-cli/commands#cache) instead.** 76 | 77 | # Configuration 78 | 79 | The config file is saved in `~/.lc/config.json`, here is a full exmaple (includes default configs): 80 | 81 | $ cat ~/.lc/config.json 82 | 83 | { 84 | "auto_login": { 85 | "enable": false 86 | }, 87 | "code": { 88 | "editor": "vim", 89 | "lang": "cpp" 90 | }, 91 | "color": { 92 | "enable": true, 93 | "theme": "default" 94 | }, 95 | "file": { 96 | "show": "${fid}.${slug}", 97 | "submission": "${fid}.${slug}.${sid}.${ac}" 98 | }, 99 | "icon": { 100 | "theme": "" 101 | }, 102 | "network": { 103 | "concurrency": 10 104 | }, 105 | "plugins": {} 106 | } 107 | 108 | Here are some useful settings: 109 | 110 | * `autologin:enable` to enable auto login feature. (see [Auto Login](#auto-login)) 111 | * `code:editor` to set editor used to open generated source file. 112 | * `code:lang` to set your default language used in coding. 113 | * `color:enable` to enable colorful output. 114 | * `color:theme` to set color theme used in output. (see [Color Theme](#color-theme)) 115 | * `file.show` to set filename pattern for generated code file. (see [File Name](#file-name)) 116 | * `icon:theme` to set icon them used in output. 117 | * `plugins` to config each installed plugins. (see [Plugins](#plugins)) 118 | 119 | **NOTE: Normally you don't need dig into the folder to manipulate those files. Use [config command](https://skygragon.github.io/leetcode-cli/commands#config) instead.** 120 | 121 | *Example* 122 | 123 | Config for `github.js` and `cpp.lint.js` plugins: 124 | 125 | { 126 | "plugins": { 127 | "github": { 128 | "repo": "https://github.com/skygragon/test", 129 | "token": "abcdefghijklmnopqrstuvwxyz" 130 | }, 131 | "cpp.lint": { 132 | "bin": "~/bin/cpplibt.py", 133 | "flags": [] 134 | } 135 | } 136 | } 137 | 138 | # Color Themes 139 | 140 | You can choose to use colorful output or not. 141 | 142 | * `--color` to enable color. 143 | * `--no-color` to disable it. 144 | 145 | Or use configuration setting to avoid typing it repeatedly. (see [color:enable](#configuration)) 146 | 147 | When color is enabled, you can choose your favor color theme as well. (see [color:theme](#configuration)) 148 | 149 | Following are available themes: 150 | 151 | * `blue` 152 | * `dark` for night. 153 | * `default` 154 | * `molokai` 155 | * `orange` 156 | * `pink` for girls. 157 | * `solarized` 158 | * `solarized.light` 159 | 160 | Of course you can create your own themes if you like, look into `colors` folder in the source code for more tips. 161 | 162 | *Example* 163 | 164 | $ cat colors/default.json 165 | { 166 | "black": "#000000", 167 | "blue": "#0000ff", 168 | "cyan": "#00ffff", 169 | "green": "#00ff00", 170 | "magenta": "#ff00ff", 171 | "red": "#ff0000", 172 | "white": "#ffffff", 173 | "yellow": "#ffff00" 174 | } 175 | 176 | # File Name 177 | 178 | You could configure file name pattern in code generation. 179 | 180 | * config `file.show` for generated file in `show`. 181 | * config `file.submission` for downloaded file in `submission`. 182 | 183 | Followings are some variables you could used in the pattern: 184 | 185 | * `${fid}` for question id. (e.g. `123`) 186 | * `${slug}` for dash-separated question name. (e.g. `add-two`) 187 | * `${name}` for space-separated questions name. (e.g. `Add Two`) 188 | * `${level}` for question level. (e.g. `Hard`) 189 | * `${category}` for question category. (e.g. `algorithms`) 190 | * `${sid}` for submission id. 191 | * `${ac}` for accept status of existing submission. 192 | 193 | # Log Levels 194 | 195 | * `-v` to enable debug output. 196 | * `-vv` to enable trace output. 197 | * Will print detailed HTTP requests/responses. 198 | 199 | # Plugins 200 | 201 | You can easily introduce more features by installing other plugins form third parties. Here lists the avaible 3rd party plugins at the moment: 202 | 203 | * [leetcode-cli-plugins](https://github.com/skygragon/leetcode-cli-plugins) 204 | 205 | Feel free to try out the plugins above. Or you can develope your own plugins to enrich leetcode-cli's functionalities. 206 | -------------------------------------------------------------------------------- /docs/demo.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Showcases 4 | --- 5 |
6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Leetcode CLI 4 | --- 5 | 11 |
12 | Installation 13 | Release Notes 14 | Showcases 15 | Commands 16 | Advanced Tips 17 |
18 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Installation 4 | --- 5 | 6 | # All in One (beta) 7 | 8 | No need to install node.js. Now available on 64bits linux, mac, and windows. 9 | 10 | [Download](https://github.com/skygragon/leetcode-cli/releases) 11 | 12 | # Prerequisites 13 | 14 | Install the latest LTS version of `node.js` (`npm` included): 15 | 16 | * [Install from package manager](https://nodejs.org/en/download/package-manager/) 17 | * [Install from directly download](https://nodejs.org/en/download/) 18 | 19 | Check before going next: 20 | 21 | $ node -v 22 | $ npm -v 23 | 24 | # Installation 25 | 26 | There are different ways to install `leetcode-cli`: 27 | 28 | ### From npm 29 | 30 | This will install the latest STABLE version, but not include the latest DEV version. 31 | 32 | $ npm install -g leetcode-cli 33 | $ leetcode version 34 | 35 | In case Ubuntu failed due to **permission denied**, try following: 36 | 37 | $ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash 38 | $ source ~/.bashrc 39 | $ nvm install --lts 40 | 41 | Find more details [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions). 42 | 43 | ### From GitHub 44 | 45 | This will install the latest DEV version from GitHub repo. 46 | 47 | $ npm install -g skygragon/leetcode-cli 48 | $ leetcode version 49 | 50 | ### From source 51 | 52 | Similar with above, while you can introduce your own changes as you wish. 53 | 54 | $ git clone http://github.com/skygragon/leetcode-cli 55 | $ cd leetcode-cli && ./bin/install 56 | $ leetcode version 57 | 58 | ### From source (all-in-one) 59 | 60 | $ git clone http://github.com/skygragon/leetcode-cli 61 | $ cd leetcode-cli && node ./bin/pkg 62 | 63 | ### From docker 64 | 65 | NOTE: This is just a tiny taste to let you feel that leetcode-cli is. Please use other ways above to install leetcode-cli if you like it. 66 | 67 | $ alias leetcode='docker run -it --rm skygragon/leetcode-cli' 68 | $ leetcode version 69 | 70 | To persistent user data, you can mount a folder like this: 71 | 72 | $ alias leetcode='docker run -it --rm -v /Users/skygragon/data:/root skygragon/leetcode-cli' 73 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetcode-tools/leetcode-cli/df69a650d212c9cb13d83ef7ba02a5f740e27b08/docs/logo.png -------------------------------------------------------------------------------- /docs/releases.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Release Notes 4 | --- 5 | # 2.6.1 6 | * `submit` 7 | * fixes 500 error on windows. 8 | 9 | # 2.6.0 10 | * build all-in-one binary for linux/macos/windows. 11 | * `show` 12 | * support customized filename. 13 | * use "--" as comment in sql file. 14 | * `list` 15 | * fixes format issue. 16 | * fixes UT failures on windows. 17 | 18 | # 2.5.4 19 | * fixes error in fresh env without .lc existed. 20 | * embed meta in file content instead of file name. 21 | * update dependencies. 22 | 23 | # 2.5.3 24 | 25 | * fixes "Failed to load locked problem" issue. 26 | * move plugin's data into separate folders: 27 | * login info 28 | * problems list 29 | * problem cache 30 | 31 | # 2.5.2 32 | 33 | * `show` 34 | * fixes 400 error 35 | * support translated content for leetcode-cn 36 | 37 | # 2.5.1 38 | 39 | * auto install missing plugins after upgrade. 40 | * use 16m colors if possible. 41 | * enhance color output on windows. 42 | * `cache` 43 | * fix issue that can't delete cache by name. 44 | * `session` 45 | * fix issue if session name is a number. 46 | * `stat` 47 | * use level weight in calendar view. 48 | 49 | # 2.5.0 50 | 51 | * add `session` command to manage coding sessions on leetcode.com. 52 | * add more color themes. 53 | * molokai 54 | * solarized 55 | * solarized.light 56 | * `list` 57 | * fix id mismatch issue. 58 | * `show` 59 | * add `-o` option to specify output folder. 60 | * fix badge output in non-default color themes. 61 | * `stat` 62 | * calculate on AC-ed questions in calendar graph. 63 | * `test` 64 | * fix out-of-order output issue. 65 | 66 | # 2.4.0 67 | 68 | * only supports node's version >= 4. 69 | * Refactor folder structure: 70 | * now `~/.lc/` would be the only folder used by leetcode-cli. 71 | * move lcconfig file to `~/.lc/`. 72 | * move cache files to `~/.lc/cache/`. 73 | * `config` 74 | * fix string value parsing error. 75 | * `list` 76 | * show tag/lang badges in `-x` output. 77 | * `show` 78 | * add `-q` `-t` options to filter random questions. 79 | * `stat` 80 | * enhance output of `-g` option. 81 | * enhance output on windows. 82 | * add `-c` option to display calendar stat of how many AC-ed questions per day. 83 | * add `--no-lock` option to filter out locked questions. 84 | * add `-q` `-t` options to filter questions stat. 85 | 86 | # 2.3.0 87 | 88 | * `plugin` 89 | * only install necessary depedencies on specific platform. 90 | * add `-c` option to show plugin config. 91 | * support [cookie.chrome](https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cookie.chrome.md) plugin. 92 | * support [cookie.firefox](https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cookie.firefox.md) plugin. 93 | * docker 94 | * support running leetcode-cli as docker container for new user's tasting. 95 | * auto build docker image in Docker Hub. 96 | * UI 97 | * Add spinner message for long time running works. 98 | * Add logo and updte documents. 99 | 100 | # 2.2.1 101 | 102 | * add commands aliases. 103 | * enhance documents about install error on Ubuntu. 104 | * `config` 105 | * fix wrong parsing on non-string value. 106 | * `plugin` 107 | * fix bug when installing new npm modules. 108 | * `show` 109 | * use traditional `.py` for python3 filename. 110 | * `submission` 111 | * enhance recursive folder creation. 112 | 113 | # 2.2.0 114 | 115 | * `config` 116 | * add new `config` command to manage user configs. 117 | * try to save user from manually editing config file (~/.lcconfig). 118 | * start to use new json config format. (NOTE: not compatible with old format!) 119 | * `show` 120 | * print suppoerted language list. 121 | * add Release Notes page. 122 | * remove several legacy hacks. 123 | 124 | # 2.1.1 125 | * `show` 126 | * add `-e` option to open editor for coding. 127 | * add `-c` option to display source code only. 128 | * remove legacy `-t` `-d` options. 129 | * fix bad alignment in colorful output. 130 | * `list` 131 | * enhance `-t` option to support multiple tags, e.g. `leetcode list -t google -t array` 132 | * support latest `company` plugin to filter questions by tags like `array` or `dynamic programming` 133 | * config 134 | * add `EDITOR` to set default editor. 135 | * fix `--no-color` bug. 136 | 137 | 138 | # 2.1.0 139 | * `show` 140 | * fix "unknown language" error due to recent API changes on leetcode.com. 141 | * add `kotlin` language. 142 | * `cache` 143 | * remove `-a` option, now `leetcode cache -d` will directly clear all cache. 144 | * add keyword match, e.g. `leetcode cache 537` will only show the cache for question 537. 145 | * update most libray depedencies. 146 | -------------------------------------------------------------------------------- /docs/screenshots/intro.2018.01.13.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetcode-tools/leetcode-cli/df69a650d212c9cb13d83ef7ba02a5f740e27b08/docs/screenshots/intro.2018.01.13.gif -------------------------------------------------------------------------------- /docs/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leetcode-tools/leetcode-cli/df69a650d212c9cb13d83ef7ba02a5f740e27b08/docs/workflow.png -------------------------------------------------------------------------------- /icons/ascii.json: -------------------------------------------------------------------------------- 1 | { 2 | "yes": "v", 3 | "no": "X", 4 | "like": "*", 5 | "unlike": " ", 6 | "lock": "$", 7 | "nolock": " ", 8 | "empty": " ", 9 | "ac": "O", 10 | "notac": "X", 11 | "none": "o" 12 | } 13 | -------------------------------------------------------------------------------- /icons/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "yes": "✔", 3 | "no": "✘", 4 | "like": "★", 5 | "unlike": "☆", 6 | "lock": "🔒", 7 | "nolock": " ", 8 | "empty": " ", 9 | "ac": "▣", 10 | "notac": "▤", 11 | "none": "⬚" 12 | } 13 | -------------------------------------------------------------------------------- /icons/win7.json: -------------------------------------------------------------------------------- 1 | { 2 | "yes": "√", 3 | "no": "×", 4 | "like": "♥", 5 | "unlike": " ", 6 | "lock": "$", 7 | "nolock": " ", 8 | "empty": " ", 9 | "ac": "O", 10 | "notac": "X", 11 | "none": "o" 12 | } 13 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | 4 | var file = require('./file'); 5 | 6 | const cache = {}; 7 | 8 | cache.init = function() { 9 | file.mkdir(file.cacheDir()); 10 | }; 11 | 12 | cache.deleteAll = function () { 13 | cache.list().forEach(value => { 14 | cache.del(value.name); 15 | }) 16 | }; 17 | 18 | cache.get = function(k) { 19 | const fullpath = file.cacheFile(k); 20 | if (!file.exist(fullpath)) return null; 21 | 22 | return JSON.parse(file.data(fullpath)); 23 | }; 24 | 25 | cache.set = function(k, v) { 26 | const fullpath = file.cacheFile(k); 27 | file.write(fullpath, JSON.stringify(v)); 28 | return true; 29 | }; 30 | 31 | cache.del = function(k) { 32 | const fullpath = file.cacheFile(k); 33 | if (!file.exist(fullpath)) return false; 34 | 35 | file.rm(fullpath); 36 | return true; 37 | }; 38 | 39 | cache.list = function() { 40 | return file.list(file.cacheDir()) 41 | .filter(x => path.extname(x) === '.json') 42 | .map(function(filename) { 43 | const k = path.basename(filename, '.json'); 44 | const stat = file.stat(file.cacheFile(k)); 45 | return { 46 | name: k, 47 | size: stat.size, 48 | mtime: stat.mtime 49 | }; 50 | }); 51 | }; 52 | 53 | module.exports = cache; 54 | -------------------------------------------------------------------------------- /lib/chalk.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | var style = require('ansi-styles'); 4 | var supportsColor = require('supports-color'); 5 | 6 | var file = require('./file'); 7 | 8 | const chalk = { 9 | enabled: supportsColor.stdout, 10 | use256: supportsColor.stdout && supportsColor.stdout.has256, 11 | use16m: supportsColor.stdout && supportsColor.stdout.has16m, 12 | themes: new Map(), 13 | theme: {} 14 | }; 15 | 16 | const pres = []; 17 | const posts = []; 18 | 19 | const DEFAULT = { 20 | black: '#000000', 21 | blue: '#0000ff', 22 | cyan: '#00ffff', 23 | gray: '#999999', 24 | green: '#00ff00', 25 | magenta: '#ff00ff', 26 | red: '#ff0000', 27 | white: '#ffffff', 28 | yellow: '#ffff00' 29 | }; 30 | 31 | chalk.setTheme = function(name) { 32 | this.theme = this.themes.get(name) || this.themes.get('default'); 33 | }; 34 | 35 | chalk.sprint = function(s, hex) { 36 | const color = chalk.use16m ? style.color.ansi16m.hex(hex) 37 | : chalk.use256 ? style.color.ansi256.hex(hex) 38 | : style.color.ansi.hex(hex); 39 | return color + s + style.color.close; 40 | }; 41 | 42 | chalk.print = function(s) { 43 | s = this.enabled ? pres.join('') + s + posts.join('') : s; 44 | pres.length = posts.length = 0; 45 | return s; 46 | }; 47 | 48 | chalk.wrap = function(pre, post) { 49 | pres.push(pre); 50 | posts.unshift(post); 51 | const f = x => chalk.print(x); 52 | Object.setPrototypeOf(f, chalk); 53 | return f; 54 | }; 55 | 56 | const bgName = x => 'bg' + x[0].toUpperCase() + x.substr(1); 57 | 58 | chalk.init = function() { 59 | for (let f of file.listCodeDir('colors')) { 60 | const theme = {}; 61 | const data = _.extendOwn({}, DEFAULT, f.data); 62 | for (let x of _.pairs(data)) { 63 | const k = x[0]; 64 | const v = x[1]; 65 | const bgK = bgName(k); 66 | 67 | if (chalk.use16m) { 68 | theme[k] = style.color.ansi16m.hex(v); 69 | theme[bgK] = style.bgColor.ansi16m.hex(v); 70 | } else if (chalk.use256) { 71 | theme[k] = style.color.ansi256.hex(v); 72 | theme[bgK] = style.bgColor.ansi256.hex(v); 73 | } else { 74 | theme[k] = style.color.ansi.hex(v); 75 | theme[bgK] = style.bgColor.ansi.hex(v); 76 | } 77 | } 78 | chalk.themes.set(f.name, theme); 79 | } 80 | 81 | for (let color of ['black', 'blue', 'cyan', 'gray', 'green', 'magenta', 'red', 'white', 'yellow']) { 82 | Object.defineProperty(chalk, color, { 83 | get: () => chalk.wrap(chalk.theme[color], style.color.close), 84 | configurable: true 85 | }); 86 | const bgcolor = bgName(color); 87 | Object.defineProperty(chalk, bgcolor, { 88 | get: () => chalk.wrap(chalk.theme[bgcolor], style.bgColor.close), 89 | configurable: true 90 | }); 91 | } 92 | 93 | for (let modifier of ['bold', 'dim', 'italic', 'inverse', 'strikethrough', 'underline']) { 94 | Object.defineProperty(chalk, modifier, { 95 | get: () => chalk.wrap(style[modifier].open, style[modifier].close), 96 | configurable: true 97 | }); 98 | } 99 | }; 100 | 101 | module.exports = chalk; 102 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | 4 | var chalk = require('./chalk'); 5 | var cache = require('./cache'); 6 | var config = require('./config'); 7 | var h = require('./helper'); 8 | var file = require('./file'); 9 | var icon = require('./icon'); 10 | var log = require('./log'); 11 | var Plugin = require('./plugin'); 12 | 13 | // We are expecting a tier configuration like: 14 | // global config < local config < cli params 15 | // Color is a tricky one so we manually handle it here. 16 | function initColor() { 17 | chalk.enabled = config.color.enable && chalk.enabled; 18 | chalk.init(); 19 | chalk.setTheme(config.color.theme); 20 | } 21 | 22 | function initIcon() { 23 | icon.init(); 24 | icon.setTheme(config.icon.theme); 25 | } 26 | 27 | function initLogLevel() { 28 | log.init(); 29 | 30 | let level = 'INFO'; 31 | if (process.argv.indexOf('-v') >= 0) level = 'DEBUG'; 32 | if (process.argv.indexOf('-vv') >= 0) level = 'TRACE'; 33 | 34 | // print HTTP details in TRACE 35 | if (level === 'TRACE') { 36 | const request = require('request'); 37 | request.debug = true; 38 | 39 | console.error = _.wrap(console.error, function(func) { 40 | let args = Array.from(arguments); 41 | args.shift(); 42 | 43 | // FIXME: hack HTTP request log, hope no one else use it... 44 | if (args.length > 0 && args[0].indexOf('REQUEST ') === 0) { 45 | args = args.map((x) => h.printSafeHTTP(x)); 46 | log.trace.apply(log, args); 47 | } else { 48 | log.info.apply(log, args); 49 | } 50 | }); 51 | } 52 | 53 | log.setLevel(level); 54 | } 55 | 56 | function initDir() { 57 | file.init(); 58 | file.mkdir(file.homeDir()) 59 | } 60 | 61 | function initPlugins(cb) { 62 | if (Plugin.init()) { 63 | Plugin.save(); 64 | return cb(); 65 | } else { 66 | Plugin.installMissings(function(e) { 67 | if (e) return cb(e); 68 | Plugin.init(); 69 | return cb(); 70 | }); 71 | } 72 | } 73 | 74 | var cli = {}; 75 | 76 | function runCommand() { 77 | var yargs = require('yargs'); 78 | h.width = yargs.terminalWidth(); 79 | yargs.commandDir('commands') 80 | .completion() 81 | .help('h') 82 | .alias('h', 'help') 83 | .version(false) 84 | .epilog('Seek more help at https://skygragon.github.io/leetcode-cli/commands') 85 | .wrap(Math.min(h.width, 120)) 86 | .argv; 87 | } 88 | 89 | cli.run = function() { 90 | process.stdout.on('error', function(e) { 91 | if (e.code === 'EPIPE') process.exit(); 92 | }); 93 | 94 | config.init(); 95 | 96 | initColor(); 97 | initIcon(); 98 | initLogLevel(); 99 | initDir() 100 | initPlugins(function(e) { 101 | if (e) return log.fatal(e); 102 | cache.init(); 103 | runCommand(); 104 | }); 105 | }; 106 | 107 | module.exports = cli; 108 | -------------------------------------------------------------------------------- /lib/commands/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | 4 | var h = require('../helper'); 5 | var chalk = require('../chalk'); 6 | var log = require('../log'); 7 | var cache = require('../cache'); 8 | var session = require('../session'); 9 | var sprintf = require('../sprintf'); 10 | 11 | const cmd = { 12 | command: 'cache [keyword]', 13 | desc: 'Manage local cache', 14 | builder: function(yargs) { 15 | return yargs 16 | .option('d', { 17 | alias: 'delete', 18 | type: 'boolean', 19 | describe: 'Delete cache by keyword', 20 | default: false 21 | }) 22 | .positional('keyword', { 23 | type: 'string', 24 | describe: 'Cache name or question id', 25 | default: '' 26 | }) 27 | .example(chalk.yellow('leetcode cache'), 'Show all cache') 28 | .example(chalk.yellow('leetcode cache 1'), 'Show cache of question 1') 29 | .example('', '') 30 | .example(chalk.yellow('leetcode cache -d'), 'Delete all cache') 31 | .example(chalk.yellow('leetcode cache 1 -d'), 'Delete cache of question 1'); 32 | } 33 | }; 34 | 35 | cmd.handler = function(argv) { 36 | session.argv = argv; 37 | 38 | const name = argv.keyword; 39 | const isInteger = Number.isInteger(Number(name)); 40 | 41 | const caches = cache.list() 42 | .filter(function(f) { 43 | return (name.length === 0) || 44 | (isInteger ? f.name.startsWith(name + '.') : f.name === name); 45 | }); 46 | 47 | if (argv.delete) { 48 | for (let f of caches) cache.del(f.name); 49 | } else { 50 | log.info(chalk.gray(sprintf(' %s %63s %s', 'Cache', 'Size', 'Created'))); 51 | log.info(chalk.gray('-'.repeat(86))); 52 | 53 | _.sortBy(caches, function(f) { 54 | let x = parseInt(f.name.split('.')[0], 10); 55 | if (Number.isNaN(x)) x = 0; 56 | return x; 57 | }) 58 | .forEach(function(f) { 59 | log.printf(' %-60s %8s %s ago', 60 | chalk.green(f.name), 61 | h.prettySize(f.size), 62 | h.prettyTime((Date.now() - f.mtime) / 1000)); 63 | }); 64 | } 65 | }; 66 | 67 | module.exports = cmd; 68 | -------------------------------------------------------------------------------- /lib/commands/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | var nconf = require('nconf'); 4 | 5 | var file = require('../file'); 6 | var chalk = require('../chalk'); 7 | var config = require('../config'); 8 | var log = require('../log'); 9 | var session = require('../session'); 10 | 11 | const cmd = { 12 | command: 'config [key] [value]', 13 | aliases: ['conf', 'cfg', 'setting'], 14 | desc: 'Manage user configs', 15 | builder: function(yargs) { 16 | return yargs 17 | .option('a', { 18 | alias: 'all', 19 | type: 'boolean', 20 | describe: 'Show all config', 21 | default: false 22 | }) 23 | .option('d', { 24 | alias: 'delete', 25 | type: 'boolean', 26 | describe: 'Delete config by key', 27 | default: false 28 | }) 29 | .positional('key', { 30 | type: 'string', 31 | describe: 'Config key, delimited by colon', 32 | default: '' 33 | }) 34 | .positional('value', { 35 | type: 'string', 36 | describe: 'Config value', 37 | default: '' 38 | }) 39 | .example(chalk.yellow('leetcode config'), 'Show user configs') 40 | .example(chalk.yellow('leetcode config -a'), 'Show all configs = user + default') 41 | .example(chalk.yellow('leetcode config plugins:github'), 'Show config by key') 42 | .example('', '') 43 | .example(chalk.yellow('leetcode config plugins:github:repo "your repo URL"'), 'Set config by key') 44 | .example(chalk.yellow('leetcode config plugins:github -d'), 'Delete config by key'); 45 | } 46 | }; 47 | 48 | function prettyConfig(cfg) { 49 | return JSON.stringify(cfg, null, 2); 50 | } 51 | 52 | function loadConfig(showall) { 53 | const cfg = showall ? config.getAll(true) : nconf.get(); 54 | return _.omit(cfg, 'type'); 55 | } 56 | 57 | function saveConfig() { 58 | file.write(file.configFile(), prettyConfig(loadConfig(false))); 59 | } 60 | 61 | cmd.handler = function(argv) { 62 | session.argv = argv; 63 | nconf.file('local', file.configFile()); 64 | 65 | // show all 66 | if (argv.key.length === 0) 67 | return log.info(prettyConfig(loadConfig(argv.all))); 68 | 69 | // sugar: notice user that use ':' instead of '.' 70 | if (argv.key.includes('.') && !argv.key.includes(':')) 71 | return log.printf('Key should use colon(:) as the delimiter, do you mean %s?', 72 | chalk.yellow(argv.key.replace(/\./g, ':'))); 73 | 74 | const v = nconf.get(argv.key); 75 | 76 | // delete 77 | if (argv.delete) { 78 | if (v === undefined) return log.fatal('Key not found: ' + argv.key); 79 | nconf.clear(argv.key); 80 | return saveConfig(); 81 | } 82 | 83 | // show 84 | if (argv.value.length === 0) { 85 | if (v === undefined) return log.fatal('Key not found: ' + argv.key); 86 | return log.info(prettyConfig(v)); 87 | } 88 | 89 | // set 90 | try { 91 | nconf.set(argv.key, JSON.parse(argv.value)); 92 | } catch (e) { 93 | nconf.set(argv.key, JSON.parse('"' + argv.value + '"')); 94 | } 95 | return saveConfig(); 96 | }; 97 | 98 | module.exports = cmd; 99 | -------------------------------------------------------------------------------- /lib/commands/list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | 4 | var h = require('../helper'); 5 | var chalk = require('../chalk'); 6 | var icon = require('../icon'); 7 | var log = require('../log'); 8 | var core = require('../core'); 9 | var session = require('../session'); 10 | 11 | const cmd = { 12 | command: 'list [keyword]', 13 | aliases: ['ls'], 14 | desc: 'List questions', 15 | builder: function(yargs) { 16 | return yargs 17 | .option('q', core.filters.query) 18 | .option('s', { 19 | alias: 'stat', 20 | type: 'boolean', 21 | default: false, 22 | describe: 'Show statistics of listed questions' 23 | }) 24 | .option('t', core.filters.tag) 25 | .option('x', { 26 | alias: 'extra', 27 | type: 'boolean', 28 | default: false, 29 | describe: 'Show extra details: category, companies, tags.' 30 | }) 31 | .option('T', { 32 | alias: 'dontTranslate', 33 | type: 'boolean', 34 | default: false, 35 | describe: 'Set to true to disable endpoint\'s translation', 36 | }) 37 | .positional('keyword', { 38 | type: 'string', 39 | default: '', 40 | describe: 'Filter questions by keyword' 41 | }) 42 | .example(chalk.yellow('leetcode list'), 'List all questions') 43 | .example(chalk.yellow('leetcode list -x'), 'Show extra info of questions, e.g. tags') 44 | .example('', '') 45 | .example(chalk.yellow('leetcode list array'), 'List questions that has "array" in name') 46 | .example(chalk.yellow('leetcode list -q eD'), 'List questions that with easy level and not done') 47 | .example(chalk.yellow('leetcode list -t google'), 'List questions from Google company (require plugin)') 48 | .example(chalk.yellow('leetcode list -t stack'), 'List questions realted to stack (require plugin)'); 49 | } 50 | }; 51 | 52 | cmd.handler = function(argv) { 53 | session.argv = argv; 54 | core.filterProblems(argv, function(e, problems) { 55 | if (e) return log.fail(e); 56 | 57 | const word = argv.keyword.toLowerCase(); 58 | if (word) { 59 | if (word.endsWith(word.substr(-1).repeat(6))) { 60 | log.warn('Hmmm...you might need a new keyboard?'); 61 | } 62 | problems = problems.filter(x => x.name.toLowerCase().includes(word)); 63 | } 64 | 65 | const stat = {}; 66 | for (let x of ['locked', 'starred', 'ac', 'notac', 'None', 'Easy', 'Medium', 'Hard']) stat[x] = 0; 67 | 68 | problems = _.sortBy(problems, x => -x.fid); 69 | for (let problem of problems) { 70 | stat[problem.level] = (stat[problem.level] || 0) + 1; 71 | stat[problem.state] = (stat[problem.state] || 0) + 1; 72 | if (problem.locked) ++stat.locked; 73 | if (problem.starred) ++stat.starred; 74 | 75 | log.printf('%s %s %s [%=4s] %-60s %-6s (%s %%)', 76 | (problem.starred ? chalk.yellow(icon.like) : icon.empty), 77 | (problem.locked ? chalk.red(icon.lock) : icon.nolock), 78 | h.prettyState(problem.state), 79 | problem.fid, 80 | problem.name, 81 | h.prettyLevel(problem.level), 82 | (problem.percent || 0).toFixed(2)); 83 | 84 | if (argv.extra) { 85 | let badges = [problem.category]; 86 | badges = badges.concat(problem.companies || []); 87 | badges = badges.concat(problem.tags || []); 88 | 89 | let buf = []; 90 | let len = 0; 91 | for (let x of badges) { 92 | if (len + x.length + 3 >= 60) { 93 | log.printf('%12s%s', ' ', chalk.gray(buf.join(' | '))); 94 | buf = []; 95 | len = 0; 96 | } 97 | buf.push(x); 98 | len += x.length + 3; 99 | } 100 | if (buf.length > 0) 101 | log.printf('%12s%s', ' ', chalk.gray(buf.join(' | '))); 102 | } 103 | } 104 | 105 | if (argv.stat) { 106 | log.info(); 107 | log.printf(' Listed: %-9s Locked: %-9s Starred: %-9s', problems.length, stat.locked, stat.starred); 108 | log.printf(' Accept: %-9s Not-AC: %-9s Remain: %-9s', stat.ac, stat.notac, stat.None); 109 | log.printf(' Easy: %-9s Medium: %-9s Hard: %-9s', stat.Easy, stat.Medium, stat.Hard); 110 | } 111 | }); 112 | }; 113 | 114 | module.exports = cmd; 115 | -------------------------------------------------------------------------------- /lib/commands/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var h = require('../helper'); 3 | var chalk = require('../chalk'); 4 | var config = require('../config'); 5 | var log = require('../log'); 6 | var Plugin = require('../plugin'); 7 | var session = require('../session'); 8 | var sprintf = require('../sprintf'); 9 | 10 | const cmd = { 11 | command: 'plugin [name]', 12 | aliases: ['extension', 'ext'], 13 | desc: 'Manage plugins', 14 | builder: function(yargs) { 15 | return yargs 16 | .option('c', { 17 | alias: 'config', 18 | type: 'boolean', 19 | describe: 'Show plugin config', 20 | default: false 21 | }) 22 | .option('d', { 23 | alias: 'disable', 24 | type: 'boolean', 25 | describe: 'Disable plugin', 26 | default: false 27 | }) 28 | .option('D', { 29 | alias: 'delete', 30 | type: 'boolean', 31 | describe: 'Delete plugin', 32 | default: false 33 | }) 34 | .option('e', { 35 | alias: 'enable', 36 | type: 'boolean', 37 | describe: 'Enable plugin', 38 | default: false 39 | }) 40 | .option('i', { 41 | alias: 'install', 42 | type: 'boolean', 43 | describe: 'Install plugin', 44 | default: false 45 | }) 46 | .positional('name', { 47 | type: 'string', 48 | describe: 'Filter plugin by name', 49 | default: '' 50 | }) 51 | .example(chalk.yellow('leetcode plugin'), 'Show all plugins') 52 | .example(chalk.yellow('leetcode plugin company'), 'Show company plugin') 53 | .example(chalk.yellow('leetcode plugin company -c'), 'Show config of company plugin') 54 | .example('', '') 55 | .example(chalk.yellow('leetcode plugin -i'), 'Install all missing plugins from GitHub') 56 | .example(chalk.yellow('leetcode plugin -i company'), 'Install company plugin from GitHub') 57 | .example(chalk.yellow('leetcode plugin -d company'), 'Disable company plugin') 58 | .example(chalk.yellow('leetcode plugin -e company'), 'Enable company plugin') 59 | .example(chalk.yellow('leetcode plugin -D company'), 'Delete company plugin'); 60 | } 61 | }; 62 | 63 | function print(plugins) { 64 | log.info(chalk.gray(sprintf(' %6s %-18s %-15s %s', 'Active', 'Name', 'Version', 'Desc'))); 65 | log.info(chalk.gray('-'.repeat(100))); 66 | 67 | plugins = plugins || Plugin.plugins; 68 | for (let p of plugins) 69 | log.printf(' %s %-18s %-15s %s', 70 | h.prettyText('', p.enabled && !p.missing), 71 | p.name, p.ver, p.desc); 72 | } 73 | 74 | cmd.handler = function(argv) { 75 | session.argv = argv; 76 | 77 | let plugins = Plugin.plugins; 78 | const name = argv.name; 79 | 80 | if (argv.install) { 81 | const cb = function(e, p) { 82 | if (e) return log.fatal(e); 83 | p.help(); 84 | p.save(); 85 | Plugin.init(); 86 | print(); 87 | }; 88 | 89 | if (name) { 90 | Plugin.install(name, cb); 91 | } else { 92 | Plugin.installMissings(cb); 93 | } 94 | return; 95 | } 96 | 97 | if (name) plugins = plugins.filter(x => x.name === name); 98 | if (plugins.length === 0) return log.fatal('Plugin not found!'); 99 | 100 | const p = plugins[0]; 101 | if (p.missing && (argv.enable || argv.disable)) 102 | return log.fatal('Plugin missing, install it first'); 103 | 104 | if (argv.enable) { 105 | p.enabled = true; 106 | p.save(); 107 | print(); 108 | } else if (argv.disable) { 109 | p.enabled = false; 110 | p.save(); 111 | print(); 112 | } else if (argv.delete) { 113 | p.delete(); 114 | p.save(); 115 | Plugin.init(); 116 | print(); 117 | } else if (argv.config) { 118 | log.info(JSON.stringify(config.plugins[name] || {}, null, 2)); 119 | } else { 120 | print(plugins); 121 | } 122 | }; 123 | 124 | module.exports = cmd; 125 | -------------------------------------------------------------------------------- /lib/commands/session.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var prompt = require('prompt'); 3 | 4 | var h = require('../helper'); 5 | var chalk = require('../chalk'); 6 | var log = require('../log'); 7 | var core = require('../core'); 8 | var session = require('../session'); 9 | var sprintf = require('../sprintf'); 10 | 11 | const cmd = { 12 | command: 'session [keyword]', 13 | aliases: ['branch'], 14 | desc: 'Manage sessions', 15 | builder: function(yargs) { 16 | return yargs 17 | .option('c', { 18 | alias: 'create', 19 | type: 'boolean', 20 | describe: 'Create session', 21 | default: false 22 | }) 23 | .option('d', { 24 | alias: 'delete', 25 | type: 'boolean', 26 | describe: 'Delete session', 27 | default: false 28 | }) 29 | .option('e', { 30 | alias: 'enable', 31 | type: 'boolean', 32 | describe: 'Enable/activate session', 33 | default: false 34 | }) 35 | .positional('keyword', { 36 | type: 'string', 37 | describe: 'Session name or id', 38 | default: '' 39 | }) 40 | .example(chalk.yellow('leetcode session'), 'Show all cache') 41 | .example(chalk.yellow('leetcode session xxx'), 'Show session by keyword') 42 | .example('', '') 43 | .example(chalk.yellow('leetcode session -c xxx'), 'Create session with name') 44 | .example(chalk.yellow('leetcode session -e xxx'), 'Enable session by keyword') 45 | .example(chalk.yellow('leetcode session -d xxx'), 'Delete session by keyword'); 46 | } 47 | }; 48 | 49 | function printSessions(e, sessions) { 50 | if (e) return log.fail(e); 51 | 52 | log.info(chalk.gray(sprintf(' %6s %5s %18s %28s %16s', 53 | 'Active', 'Id', 'Name', 'AC Questions', 'AC Submits'))); 54 | log.info(chalk.gray('-'.repeat(80))); 55 | 56 | for (let s of sessions) { 57 | let questionRate = 0; 58 | let submissionRate = 0; 59 | if (s.submitted_questions > 0) 60 | questionRate = s.ac_questions * 100 / s.submitted_questions; 61 | if (s.total_submitted > 0) 62 | submissionRate = s.total_acs * 100 / s.total_submitted; 63 | 64 | log.printf(' %s %8s %-26s %6s (%6s %%) %6s (%6s %%)', 65 | s.is_active ? h.prettyState('ac') : ' ', 66 | s.id, 67 | s.name || 'Anonymous Session', 68 | chalk.green(s.ac_questions), 69 | questionRate.toFixed(2), 70 | chalk.green(s.total_acs), 71 | submissionRate.toFixed(2)); 72 | } 73 | } 74 | 75 | cmd.handler = function(argv) { 76 | session.argv = argv; 77 | 78 | if (argv.create) 79 | return core.createSession(argv.keyword, printSessions); 80 | 81 | core.getSessions(function(e, sessions) { 82 | if (e) return log.fail(e); 83 | 84 | if (argv.keyword) { 85 | const id = Number(argv.keyword); 86 | sessions = sessions.filter(x => x.name === argv.keyword || x.id === id); 87 | if (sessions.length > 1) return log.fail('Ambiguous sessions?'); 88 | 89 | const session = sessions[0]; 90 | if (!session) return log.fail('Session not found!'); 91 | 92 | if (argv.enable && !session.is_active) { 93 | core.activateSession(session, function(e, sessions) { 94 | if (e) return log.fail(e); 95 | require('../session').deleteCodingSession(); 96 | printSessions(e, sessions); 97 | }); 98 | return; 99 | } 100 | 101 | if (argv.delete) { 102 | return core.deleteSession(session, printSessions); 103 | } 104 | } 105 | printSessions(null, sessions); 106 | }); 107 | }; 108 | 109 | module.exports = cmd; 110 | -------------------------------------------------------------------------------- /lib/commands/show.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | 4 | var _ = require('underscore'); 5 | var childProcess = require('child_process'); 6 | 7 | var h = require('../helper'); 8 | var file = require('../file'); 9 | var chalk = require('../chalk'); 10 | var icon = require('../icon'); 11 | var log = require('../log'); 12 | var config = require('../config'); 13 | var core = require('../core'); 14 | var session = require('../session'); 15 | 16 | const cmd = { 17 | command: 'show [keyword]', 18 | aliases: ['view', 'pick'], 19 | desc: 'Show question', 20 | builder: function(yargs) { 21 | return yargs 22 | .option('c', { 23 | alias: 'codeonly', 24 | type: 'boolean', 25 | default: false, 26 | describe: 'Only show code template' 27 | }) 28 | .option('e', { 29 | alias: 'editor', 30 | type: 'string', 31 | describe: 'Open source code in editor' 32 | }) 33 | .option('g', { 34 | alias: 'gen', 35 | type: 'boolean', 36 | default: false, 37 | describe: 'Generate source code' 38 | }) 39 | .option('l', { 40 | alias: 'lang', 41 | type: 'string', 42 | default: config.code.lang, 43 | describe: 'Programming language of the source code', 44 | choices: config.sys.langs 45 | }) 46 | .option('o', { 47 | alias: 'outdir', 48 | type: 'string', 49 | describe: 'Where to save source code', 50 | default: '.' 51 | }) 52 | .option('q', core.filters.query) 53 | .option('t', core.filters.tag) 54 | .option('x', { 55 | alias: 'extra', 56 | type: 'boolean', 57 | default: false, 58 | describe: 'Show extra question details in source code' 59 | }) 60 | .option('T', { 61 | alias: 'dontTranslate', 62 | type: 'boolean', 63 | default: false, 64 | describe: 'Set to true to disable endpoint\'s translation', 65 | }) 66 | .positional('keyword', { 67 | type: 'string', 68 | default: '', 69 | describe: 'Show question by name or id' 70 | }) 71 | .example(chalk.yellow('leetcode show 1'), 'Show question 1') 72 | .example(chalk.yellow('leetcode show 1 -gx -l java'), 'Show question 1 and generate Java code') 73 | .example(chalk.yellow('leetcode show 1 -gxe'), 'Open generated code in editor') 74 | .example('', '') 75 | .example(chalk.yellow('leetcode show'), 'Show random question') 76 | .example(chalk.yellow('leetcode show -q h'), 'Show random hard question') 77 | .example(chalk.yellow('leetcode show -t google'), 'Show random question from Google (require plugin)'); 78 | } 79 | }; 80 | 81 | function genFileName(problem, opts) { 82 | const path = require('path'); 83 | const params = [ 84 | file.fmt(config.file.show, problem), 85 | '', 86 | h.langToExt(opts.lang) 87 | ]; 88 | 89 | // try new name to avoid overwrite by mistake 90 | for (let i = 0; ; ++i) { 91 | const name = path.join(opts.outdir, params.join('.').replace(/\.+/g, '.')); 92 | if (!file.exist(name)) 93 | return name; 94 | params[1] = i; 95 | } 96 | } 97 | 98 | function showProblem(problem, argv) { 99 | const taglist = [problem.category] 100 | .concat(problem.companies || []) 101 | .concat(problem.tags || []) 102 | .map(x => h.badge(x, 'blue')) 103 | .join(' '); 104 | const langlist = problem.templates 105 | .map(x => h.badge(x.value, 'yellow')) 106 | .sort() 107 | .join(' '); 108 | 109 | let code; 110 | const needcode = argv.gen || argv.codeonly; 111 | if (needcode) { 112 | const template = problem.templates.find(x => x.value === argv.lang); 113 | if (!template) { 114 | log.fail('Not supported language "' + argv.lang + '"'); 115 | log.warn('Supported languages: ' + langlist); 116 | return; 117 | } 118 | 119 | const opts = { 120 | lang: argv.lang, 121 | code: template.defaultCode, 122 | tpl: argv.extra ? 'detailed' : 'codeonly' 123 | }; 124 | code = core.exportProblem(problem, opts); 125 | } 126 | 127 | let filename; 128 | if (argv.gen) { 129 | file.mkdir(argv.outdir); 130 | filename = genFileName(problem, argv); 131 | file.write(filename, code); 132 | 133 | if (argv.editor !== undefined) { 134 | childProcess.spawn(argv.editor || config.code.editor, [filename], { 135 | // in case your editor of choice is vim or emacs 136 | stdio: 'inherit' 137 | }); 138 | } 139 | } else { 140 | if (argv.codeonly) { 141 | log.info(chalk.yellow(code)); 142 | return; 143 | } 144 | } 145 | 146 | log.printf('[%s] %s %s', problem.fid, problem.name, 147 | (problem.starred ? chalk.yellow(icon.like) : icon.empty)); 148 | log.info(); 149 | log.info(chalk.underline(problem.link)); 150 | if (argv.extra) { 151 | log.info(); 152 | log.info('Tags: ' + taglist); 153 | log.info(); 154 | log.info('Langs: ' + langlist); 155 | } 156 | 157 | log.info(); 158 | log.printf('* %s', problem.category); 159 | log.printf('* %s (%s%%)', h.prettyLevel(problem.level), problem.percent.toFixed(2)); 160 | 161 | if (problem.likes) 162 | log.printf('* Likes: %s', problem.likes); 163 | if (problem.dislikes) 164 | log.printf('* Dislikes: %s', problem.dislikes); 165 | else 166 | log.printf('* Dislikes: -'); 167 | if (problem.totalAC) 168 | log.printf('* Total Accepted: %s', problem.totalAC); 169 | if (problem.totalSubmit) 170 | log.printf('* Total Submissions: %s', problem.totalSubmit); 171 | if (problem.testable && problem.testcase) 172 | log.printf('* Testcase Example: %s', chalk.yellow(util.inspect(problem.testcase))); 173 | if (filename) 174 | log.printf('* Source Code: %s', chalk.yellow.underline(filename)); 175 | 176 | log.info(); 177 | log.info(problem.desc); 178 | } 179 | 180 | cmd.handler = function(argv) { 181 | session.argv = argv; 182 | if (argv.keyword.length > 0) { 183 | // show specific one 184 | core.getProblem(argv.keyword, !argv.dontTranslate, function(e, problem) { 185 | if (e) return log.fail(e); 186 | showProblem(problem, argv); 187 | }); 188 | } else { 189 | // show random one 190 | core.filterProblems(argv, function(e, problems) { 191 | if (e) return log.fail(e); 192 | 193 | // random select one that not AC-ed yet 194 | const user = session.getUser(); 195 | problems = problems.filter(function(x) { 196 | if (x.state === 'ac') return false; 197 | if (!user.paid && x.locked) return false; 198 | return true; 199 | }); 200 | if (problems.length === 0) return log.fail('Problem not found!'); 201 | 202 | const problem = _.sample(problems); 203 | core.getProblem(problem, !argv.dontTranslate, function(e, problem) { 204 | if (e) return log.fail(e); 205 | showProblem(problem, argv); 206 | }); 207 | }); 208 | } 209 | }; 210 | 211 | module.exports = cmd; 212 | -------------------------------------------------------------------------------- /lib/commands/star.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chalk = require('../chalk'); 3 | var icon = require('../icon'); 4 | var log = require('../log'); 5 | var core = require('../core'); 6 | var session = require('../session'); 7 | 8 | const cmd = { 9 | command: 'star ', 10 | aliases: ['like', 'favorite'], 11 | desc: 'Star favorite question', 12 | builder: function(yargs) { 13 | return yargs 14 | .option('d', { 15 | alias: 'delete', 16 | type: 'boolean', 17 | describe: 'Unstar question', 18 | default: false 19 | }) 20 | .positional('keyword', { 21 | type: 'string', 22 | describe: 'Question name or id', 23 | default: '' 24 | }) 25 | .example(chalk.yellow('leetcode star 1'), 'Mark favorite to question 1') 26 | .example(chalk.yellow('leetcode star 1 -d'), 'Unmark favorite to question 1'); 27 | } 28 | }; 29 | 30 | cmd.handler = function(argv) { 31 | session.argv = argv; 32 | // translation doesn't affect question lookup 33 | core.getProblem(argv.keyword, true, function(e, problem) { 34 | if (e) return log.fail(e); 35 | 36 | core.starProblem(problem, !argv.delete, function(e, starred) { 37 | if (e) return log.fail(e); 38 | 39 | log.printf('[%s] %s %s', problem.fid, problem.name, 40 | chalk.yellow(starred ? icon.like : icon.unlike)); 41 | 42 | core.updateProblem(problem, {starred: starred}); 43 | }); 44 | }); 45 | }; 46 | 47 | module.exports = cmd; 48 | -------------------------------------------------------------------------------- /lib/commands/stat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var moment = require('moment'); 3 | var _ = require('underscore'); 4 | 5 | var chalk = require('../chalk'); 6 | var icon = require('../icon'); 7 | var log = require('../log'); 8 | var core = require('../core'); 9 | var session = require('../session'); 10 | var sprintf = require('../sprintf'); 11 | var h = require('../helper'); 12 | 13 | const cmd = { 14 | command: 'stat', 15 | desc: 'Show statistics', 16 | aliases: ['stats', 'progress', 'report'], 17 | builder: function(yargs) { 18 | return yargs 19 | .option('c', { 20 | alias: 'cal', 21 | type: 'boolean', 22 | default: false, 23 | describe: 'Show calendar statistics' 24 | }) 25 | .option('g', { 26 | alias: 'graph', 27 | type: 'boolean', 28 | default: false, 29 | describe: 'Show graphic statistics' 30 | }) 31 | .option('l', { 32 | alias: 'lock', 33 | type: 'boolean', 34 | default: true, 35 | describe: 'Include locked questions' 36 | }) 37 | .option('q', core.filters.query) 38 | .option('t', core.filters.tag) 39 | .example(chalk.yellow('leetcode stat'), 'Show progress status') 40 | .example(chalk.yellow('leetcode stat -g'), 'Show detailed status in graph') 41 | .example(chalk.yellow('leetcode stat -c'), 'Show accepted status in calendar') 42 | .example('', '') 43 | .example(chalk.yellow('leetcode stat --no-lock'), 'Show status without locked questions') 44 | .example(chalk.yellow('leetcode stat -t algorithms'), 'Show status of algorithms questions only') 45 | .example(chalk.yellow('leetcode stat -q h'), 'Show status of hard questions only'); 46 | } 47 | }; 48 | 49 | function printLine(key, done, all) { 50 | const n = 30; 51 | const percent = (all > 0) ? done / all : 0; 52 | const x = Math.ceil(n * percent); 53 | log.printf(' %s\t%3s/%-3s (%6s %%) %s%s', 54 | h.prettyLevel(key), done, all, 55 | (100 * percent).toFixed(2), 56 | chalk.green('█'.repeat(x)), 57 | chalk.red('░'.repeat(n - x))); 58 | } 59 | 60 | function showProgress(problems) { 61 | const stats = { 62 | easy: {all: 0, ac: 0}, 63 | medium: {all: 0, ac: 0}, 64 | hard: {all: 0, ac: 0} 65 | }; 66 | 67 | for (let problem of problems) { 68 | const level = problem.level.toLowerCase(); 69 | const state = problem.state.toLowerCase(); 70 | 71 | if (!(level in stats)) continue; 72 | ++stats[level].all; 73 | 74 | if (!(state in stats[level])) continue; 75 | ++stats[level][state]; 76 | } 77 | 78 | printLine('Easy', stats.easy.ac, stats.easy.all); 79 | printLine('Medium', stats.medium.ac, stats.medium.all); 80 | printLine('Hard', stats.hard.ac, stats.hard.all); 81 | } 82 | 83 | function showGraph(problems) { 84 | const ICONS = { 85 | ac: chalk.green(icon.ac), 86 | notac: chalk.red(icon.notac), 87 | none: chalk.gray(icon.none), 88 | empty: icon.empty 89 | }; 90 | 91 | // row header is 4 bytes 92 | // each question takes 2 bytes 93 | // each group has 10 questions, which takes (2*10=20) + 3 paddings 94 | let groups = Math.floor((h.width - 4) / (3 + 2 * 10)); 95 | if (groups < 1) groups = 1; 96 | if (groups > 5) groups = 5; 97 | 98 | const header = _.range(groups) 99 | .map(x => sprintf('%4s%18s', x * 10 + 1, x * 10 + 10)) 100 | .join(''); 101 | log.info(' ' + header); 102 | 103 | const graph = []; 104 | for (let problem of problems) 105 | graph[problem.fid] = ICONS[problem.state] || ICONS.none; 106 | 107 | let line = [sprintf(' %04s', 0)]; 108 | for (let i = 1, n = graph.length; i <= n; ++i) { 109 | // padding before group 110 | if (i % 10 === 1) line.push(' '); 111 | 112 | line.push(graph[i] || ICONS.empty); 113 | 114 | // time to start new row 115 | if (i % (10 * groups) === 0 || i === n) { 116 | log.info(line.join(' ')); 117 | line = [sprintf(' %04s', i)]; 118 | } 119 | } 120 | 121 | log.info(); 122 | log.printf('%7s%s%3s%s%3s%s', 123 | ' ', ICONS.ac + chalk.green(' Accepted'), 124 | ' ', ICONS.notac + chalk.red(' Not Accepted'), 125 | ' ', ICONS.none + ' Remaining'); 126 | } 127 | 128 | function showCal(problems) { 129 | const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 130 | const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 131 | const ICONS = [ 132 | icon.none, 133 | chalk.white(icon.ac), 134 | chalk.green(icon.ac), 135 | chalk.yellow(icon.ac), 136 | chalk.red(icon.ac) 137 | ]; 138 | 139 | const N_MONTHS = 12; 140 | const N_WEEKS = 53; 141 | const N_WEEKDAYS = 7; 142 | 143 | const now = moment(); 144 | 145 | const SCORES = {easy: 1, medium: 2, hard: 5}; 146 | function toScore(sum, id) { 147 | const problem = problems.find(x => x.fid === id); 148 | if (problem) sum += (SCORES[problem.level.toLowerCase()] || 1); 149 | return sum; 150 | } 151 | 152 | // load historical stats 153 | const graph = []; 154 | const stats = require('../cache').get(h.KEYS.stat) || {}; 155 | for (let k of _.keys(stats)) { 156 | const score = (stats[k]['ac.set'] || []).reduce(toScore, 0); 157 | if (score === 0) continue; 158 | 159 | const d = moment(k, 'YYYY-MM-DD'); 160 | graph[now.diff(d, 'days')] = score; 161 | } 162 | 163 | // print header 164 | const buf = Buffer.alloc(120, ' ', 'ascii'); 165 | for (let i = 0; i <= N_MONTHS; ++i) { 166 | // for day 1 in each month, calculate its column position in graph 167 | const d = now.clone().subtract(i, 'months').date(1); 168 | const idx = now.diff(d, 'days'); 169 | 170 | const j = (N_WEEKS - idx / N_WEEKDAYS + 1) * 2; 171 | if (j >= 0) buf.write(MONTHS[d.month()], j); 172 | } 173 | log.printf('%7s%s', ' ', buf.toString()); 174 | 175 | // print graph 176 | for (let i = 0; i < N_WEEKDAYS; ++i) { 177 | const line = []; 178 | // print day in week 179 | const idx = (now.day() + i + 1) % N_WEEKDAYS; 180 | line.push(sprintf('%4s ', WEEKDAYS[idx])); 181 | 182 | for (let j = 0; j < N_WEEKS; ++j) { 183 | let idx = (N_WEEKS - j - 1) * N_WEEKDAYS + N_WEEKDAYS - i - 1; 184 | const d = now.clone().subtract(idx, 'days'); 185 | 186 | // map count to icons index: 187 | // [0] => 0, [1,5] => 1, [6,10] => 2, [11,15] => 3, [16,) => 4 188 | const count = graph[idx] || 0; 189 | idx = Math.floor((count - 1) / 5) + 1; 190 | if (idx > 4) idx = 4; 191 | 192 | let icon = ICONS[idx]; 193 | // use different colors for adjacent months 194 | if (idx === 0 && d.month() % 2) icon = chalk.gray(icon); 195 | line.push(icon); 196 | } 197 | log.info(line.join(' ')); 198 | } 199 | 200 | log.info(); 201 | log.printf('%8s%s%3s%s%3s%s%3s%s', 202 | ' ', ICONS[1] + ' 1~5', 203 | ' ', ICONS[2] + ' 6~10', 204 | ' ', ICONS[3] + ' 11~15', 205 | ' ', ICONS[4] + ' 16+'); 206 | } 207 | 208 | cmd.handler = function(argv) { 209 | session.argv = argv; 210 | core.filterProblems(argv, function(e, problems) { 211 | if (e) return log.fail(e); 212 | 213 | if (!argv.lock) 214 | problems = problems.filter(x => !x.locked); 215 | 216 | log.info(); 217 | if (argv.graph) showGraph(problems); 218 | else if (argv.cal) showCal(problems); 219 | else showProgress(problems); 220 | log.info(); 221 | }); 222 | }; 223 | 224 | module.exports = cmd; 225 | -------------------------------------------------------------------------------- /lib/commands/submission.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | 4 | var _ = require('underscore'); 5 | 6 | var h = require('../helper'); 7 | var file = require('../file'); 8 | var chalk = require('../chalk'); 9 | var config = require('../config'); 10 | var log = require('../log'); 11 | var Queue = require('../queue'); 12 | var core = require('../core'); 13 | var session = require('../session'); 14 | 15 | const cmd = { 16 | command: 'submission [keyword]', 17 | aliases: ['pull'], 18 | desc: 'Download submission code', 19 | builder: function(yargs) { 20 | return yargs 21 | .option('a', { 22 | alias: 'all', 23 | type: 'boolean', 24 | default: false, 25 | describe: 'Download all questions' 26 | }) 27 | .option('l', { 28 | alias: 'lang', 29 | type: 'string', 30 | default: 'all', 31 | describe: 'Filter by programming language' 32 | }) 33 | .option('o', { 34 | alias: 'outdir', 35 | type: 'string', 36 | describe: 'Where to save submission code', 37 | default: '.' 38 | }) 39 | .option('x', { 40 | alias: 'extra', 41 | type: 'boolean', 42 | default: false, 43 | describe: 'Show extra question details in submission code' 44 | }) 45 | .option('T', { 46 | alias: 'dontTranslate', 47 | type: 'boolean', 48 | default: false, 49 | describe: 'Set to true to disable endpoint\'s translation', 50 | }) 51 | .positional('keyword', { 52 | type: 'string', 53 | default: '', 54 | describe: 'Download specific question by id' 55 | }) 56 | .example(chalk.yellow('leetcode submission -a -o mydir'), 'Download all to folder mydir') 57 | .example(chalk.yellow('leetcode submission -x -a'), 'Add descriptions in the downloaded codes') 58 | .example(chalk.yellow('leetcode submission -l cpp 1'), 'Download cpp submission of question 1'); 59 | } 60 | }; 61 | 62 | function doTask(problem, queue, cb) { 63 | const argv = queue.ctx.argv; 64 | 65 | function onTaskDone(e, msg) { 66 | // NOTE: msg color means different purpose: 67 | // - red: error 68 | // - green: accepted, fresh download 69 | // - yellow: not ac-ed, fresh download 70 | // - white: existed already, skip download 71 | log.printf('[%=4s] %-60s %s', problem.fid, problem.name, 72 | (e ? chalk.red('ERROR: ' + (e.msg || e)) : msg)); 73 | if (cb) cb(e); 74 | } 75 | 76 | if (argv.extra) { 77 | // have to get problem details, e.g. problem description. 78 | core.getProblem(problem.fid, !argv.dontTranslate, function(e, problem) { 79 | if (e) return cb(e); 80 | exportSubmission(problem, argv, onTaskDone); 81 | }); 82 | } else { 83 | exportSubmission(problem, argv, onTaskDone); 84 | } 85 | } 86 | 87 | function exportSubmission(problem, argv, cb) { 88 | core.getSubmissions(problem, function(e, submissions) { 89 | if (e) return cb(e); 90 | if (submissions.length === 0) 91 | return cb('No submissions?'); 92 | 93 | // get obj list contain required filetype 94 | submissions = submissions.filter(x => argv.lang === 'all' || argv.lang === x.lang); 95 | if (submissions.length === 0) 96 | return cb('No submissions in required language.'); 97 | 98 | // if no accepted, use the latest non-accepted one 99 | const submission = submissions.find(x => x.status_display === 'Accepted') || submissions[0]; 100 | submission.ac = (submission.status_display === 'Accepted'); 101 | 102 | const data = _.extend({}, submission, problem); 103 | data.sid = submission.id; 104 | data.ac = submission.ac ? 'ac' : 'notac'; 105 | const basename = file.fmt(config.file.submission, data); 106 | const f = path.join(argv.outdir, basename + h.langToExt(submission.lang)); 107 | 108 | file.mkdir(argv.outdir); 109 | // skip the existing cached submissions 110 | if (file.exist(f)) 111 | return cb(null, chalk.underline(f)); 112 | 113 | core.getSubmission(submission, function(e, submission) { 114 | if (e) return cb(e); 115 | 116 | const opts = { 117 | lang: submission.lang, 118 | code: submission.code, 119 | tpl: argv.extra ? 'detailed' : 'codeonly' 120 | }; 121 | file.write(f, core.exportProblem(problem, opts)); 122 | cb(null, submission.ac ? chalk.green.underline(f) 123 | : chalk.yellow.underline(f)); 124 | }); 125 | }); 126 | } 127 | 128 | cmd.handler = function(argv) { 129 | session.argv = argv; 130 | const q = new Queue(null, {argv: argv}, doTask); 131 | 132 | if (argv.all) { 133 | core.getProblems(function(e, problems) { 134 | if (e) return log.fail(e); 135 | problems = problems.filter(x => x.state === 'ac' || x.state === 'notac'); 136 | q.addTasks(problems).run(); 137 | }); 138 | return; 139 | } 140 | 141 | if (!argv.keyword) 142 | return log.fail('missing keyword?'); 143 | 144 | core.getProblem(argv.keyword, !argv.dontTranslate, function(e, problem) { 145 | if (e) return log.fail(e); 146 | q.addTask(problem).run(); 147 | }); 148 | }; 149 | 150 | module.exports = cmd; 151 | -------------------------------------------------------------------------------- /lib/commands/submit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | var lodash = require('lodash'); 4 | 5 | var h = require('../helper'); 6 | var file = require('../file'); 7 | var chalk = require('../chalk'); 8 | var log = require('../log'); 9 | var core = require('../core'); 10 | var session = require('../session'); 11 | 12 | const cmd = { 13 | command: 'submit ', 14 | aliases: ['push', 'commit'], 15 | desc: 'Submit code', 16 | builder: function(yargs) { 17 | return yargs 18 | .positional('filename', { 19 | type: 'string', 20 | describe: 'Code file to submit', 21 | default: '' 22 | }) 23 | .example(chalk.yellow('leetcode submit 1.two-sum.cpp'), 'Submit code'); 24 | } 25 | }; 26 | 27 | function printResult(actual, k) { 28 | if (!actual.hasOwnProperty(k)) return; 29 | 30 | const v = actual[k] || ''; 31 | const lines = Array.isArray(v) ? v : [v]; 32 | for (let line of lines) { 33 | if (k !== 'state') line = lodash.startCase(k) + ': ' + line; 34 | log.info(' ' + h.prettyText(' ' + line, actual.ok)); 35 | } 36 | } 37 | 38 | function printLine() { 39 | const args = Array.from(arguments); 40 | const actual = args.shift(); 41 | const line = util.format.apply(util, args); 42 | log.info(' ' + h.prettyText(' ' + line, actual.ok)); 43 | } 44 | 45 | cmd.handler = function(argv) { 46 | session.argv = argv; 47 | if (!file.exist(argv.filename)) 48 | return log.fatal('File ' + argv.filename + ' not exist!'); 49 | 50 | const meta = file.meta(argv.filename); 51 | 52 | // translation doesn't affect problem lookup 53 | core.getProblem(meta.id, true, function(e, problem) { 54 | if (e) return log.fail(e); 55 | 56 | problem.file = argv.filename; 57 | problem.lang = meta.lang; 58 | 59 | core.submitProblem(problem, function(e, results) { 60 | if (e) return log.fail(e); 61 | 62 | const result = results[0]; 63 | 64 | printResult(result, 'state'); 65 | printLine(result, '%d/%d cases passed (%s)', 66 | result.passed, result.total, result.runtime); 67 | 68 | if (result.ok) { 69 | session.updateStat('ac', 1); 70 | session.updateStat('ac.set', problem.fid); 71 | 72 | (function () { 73 | if (result.runtime_percentile) 74 | printLine(result, 'Your runtime beats %d %% of %s submissions', 75 | result.runtime_percentile.toFixed(2), result.lang); 76 | else 77 | return log.warn('Failed to get runtime percentile.'); 78 | if (result.memory && result.memory_percentile) 79 | printLine(result, 'Your memory usage beats %d %% of %s submissions (%s)', 80 | result.memory_percentile.toFixed(2), result.lang, result.memory); 81 | else 82 | return log.warn('Failed to get memory percentile.'); 83 | })(); 84 | 85 | // core.getSubmission({id: result.id}, function(e, submission) { 86 | // if (e || !submission || !submission.distributionChart) 87 | // return log.warn('Failed to get submission beat ratio.'); 88 | 89 | // const lang = submission.distributionChart.lang; 90 | // const scores = submission.distributionChart.distribution; 91 | // const myRuntime = parseFloat(result.runtime); 92 | 93 | // let ratio = 0.0; 94 | // for (let score of scores) { 95 | // if (parseFloat(score[0]) > myRuntime) 96 | // ratio += parseFloat(score[1]); 97 | // } 98 | 99 | // printLine(result, 'Your runtime beats %d %% of %s submissions', 100 | // ratio.toFixed(2), lang); 101 | // }); 102 | } else { 103 | result.testcase = result.testcase.slice(1, -1).replace(/\\n/g, '\n'); 104 | printResult(result, 'error'); 105 | printResult(result, 'testcase'); 106 | printResult(result, 'answer'); 107 | printResult(result, 'expected_answer'); 108 | printResult(result, 'stdout'); 109 | } 110 | 111 | // update this problem status in local cache 112 | core.updateProblem(problem, {state: (result.ok ? 'ac' : 'notac')}); 113 | }); 114 | }); 115 | }; 116 | 117 | module.exports = cmd; 118 | -------------------------------------------------------------------------------- /lib/commands/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | var lodash = require('lodash'); 4 | var util = require('util'); 5 | 6 | var h = require('../helper'); 7 | var file = require('../file'); 8 | var chalk = require('../chalk'); 9 | var log = require('../log'); 10 | var core = require('../core'); 11 | var session = require('../session'); 12 | 13 | const cmd = { 14 | command: 'test ', 15 | aliases: ['run'], 16 | desc: 'Test code', 17 | builder: function(yargs) { 18 | return yargs 19 | .option('i', { 20 | alias: 'interactive', 21 | type: 'boolean', 22 | default: false, 23 | describe: 'Provide test case interactively' 24 | }) 25 | .option('t', { 26 | alias: 'testcase', 27 | type: 'string', 28 | default: '', 29 | describe: 'Provide test case' 30 | }) 31 | .positional('filename', { 32 | type: 'string', 33 | default: '', 34 | describe: 'Code file to test' 35 | }) 36 | .example(chalk.yellow('leetcode test 1.two-sum.cpp'), 'Test code with default test case') 37 | .example(chalk.yellow('leetcode test 1.two-sum.cpp -t "[1,2,3]\\n4"'), 'Test code with customized test case'); 38 | } 39 | }; 40 | 41 | function printResult(actual, extra, k) { 42 | if (!actual.hasOwnProperty(k)) return; 43 | // HACk: leetcode still return 'Accepted' even the answer is wrong!! 44 | const v = actual[k] || ''; 45 | if (k === 'state' && v === 'Accepted') return; 46 | 47 | let ok = actual.ok; 48 | 49 | const lines = Array.isArray(v) ? v : [v]; 50 | for (let line of lines) { 51 | const extraInfo = extra ? ` (${extra})` : ''; 52 | if (k !== 'state') line = lodash.startCase(k) + extraInfo + ': ' + line; 53 | log.info(' ' + h.prettyText(' ' + line, ok)); 54 | } 55 | } 56 | 57 | function runTest(argv) { 58 | if (!file.exist(argv.filename)) 59 | return log.fatal('File ' + argv.filename + ' not exist!'); 60 | 61 | const meta = file.meta(argv.filename); 62 | 63 | core.getProblem(meta.id, true, function(e, problem) { 64 | if (e) return log.fail(e); 65 | 66 | if (!problem.testable) 67 | return log.fail('not testable? please submit directly!'); 68 | 69 | if (argv.testcase) 70 | problem.testcase = argv.testcase.replace(/\\n/g, '\n'); 71 | 72 | if (!problem.testcase) 73 | return log.fail('missing testcase?'); 74 | 75 | problem.file = argv.filename; 76 | problem.lang = meta.lang; 77 | 78 | core.testProblem(problem, function(e, results) { 79 | if (e) return log.fail(e); 80 | 81 | results = _.sortBy(results, x => x.type); 82 | if (results[0].state === 'Accepted') 83 | results[0].state = 'Finished'; 84 | printResult(results[0], null, 'state'); 85 | printResult(results[0], null, 'error'); 86 | 87 | results[0].your_input = problem.testcase; 88 | results[0].output = results[0].answer; 89 | // LeetCode-CN returns the actual and expected answer into two separate responses 90 | if (results[1]) { 91 | results[0].expected_answer = results[1].answer; 92 | } 93 | results[0].stdout = results[0].stdout.slice(1, -1).replace(/\\n/g, '\n'); 94 | printResult(results[0], null, 'your_input'); 95 | printResult(results[0], results[0].runtime, 'output'); 96 | printResult(results[0], null, 'expected_answer'); 97 | printResult(results[0], null, 'stdout'); 98 | }); 99 | }); 100 | } 101 | 102 | cmd.handler = function(argv) { 103 | session.argv = argv; 104 | if (!argv.i) 105 | return runTest(argv); 106 | 107 | h.readStdin(function(e, data) { 108 | if (e) return log.fail(e); 109 | 110 | argv.testcase = data; 111 | return runTest(argv); 112 | }); 113 | }; 114 | 115 | module.exports = cmd; 116 | -------------------------------------------------------------------------------- /lib/commands/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var prompt = require('prompt'); 3 | 4 | var h = require('../helper'); 5 | var config = require('../config'); 6 | var chalk = require('../chalk'); 7 | var log = require('../log'); 8 | var core = require('../core'); 9 | var session = require('../session'); 10 | var sprintf = require('../sprintf'); 11 | 12 | const cmd = { 13 | command: 'user', 14 | aliases: ['account'], 15 | desc: 'Manage account', 16 | builder: function(yargs) { 17 | return yargs 18 | .option('l', { 19 | alias: 'login', 20 | type: 'boolean', 21 | default: false, 22 | describe: 'Login' 23 | }) 24 | .option('c', { 25 | alias: 'cookie', 26 | type: 'boolean', 27 | default: false, 28 | describe: 'cookieLogin' 29 | }) 30 | .option('g', { 31 | alias: 'github', 32 | type: 'boolean', 33 | default: false, 34 | describe: 'githubLogin' 35 | }) 36 | .option('i', { 37 | alias: 'linkedin', 38 | type: 'boolean', 39 | default: false, 40 | describe: 'linkedinLogin' 41 | }) 42 | .option('L', { 43 | alias: 'logout', 44 | type: 'boolean', 45 | default: false, 46 | describe: 'Logout' 47 | }) 48 | .example(chalk.yellow('leetcode user'), 'Show current user') 49 | .example(chalk.yellow('leetcode user -l'), 'User login') 50 | .example(chalk.yellow('leetcode user -c'), 'User Cookie login') 51 | .example(chalk.yellow('leetcode user -g'), 'User GitHub login') 52 | .example(chalk.yellow('leetcode user -i'), 'User LinkedIn login') 53 | .example(chalk.yellow('leetcode user -L'), 'User logout'); 54 | } 55 | }; 56 | 57 | cmd.handler = function(argv) { 58 | session.argv = argv; 59 | let user = null; 60 | if (argv.login) { 61 | // login 62 | prompt.colors = false; 63 | prompt.message = ''; 64 | prompt.start(); 65 | prompt.get([ 66 | {name: 'login', required: true}, 67 | {name: 'pass', required: true, hidden: true} 68 | ], function(e, user) { 69 | if (e) return log.fail(e); 70 | 71 | core.login(user, function(e, user) { 72 | if (e) return log.fail(e); 73 | log.info('Successfully login as', chalk.yellow(user.name)); 74 | }); 75 | }); 76 | } else if (argv.logout) { 77 | // logout 78 | user = core.logout(user, true); 79 | if (user) 80 | log.info('Successfully logout as', chalk.yellow(user.name)); 81 | else 82 | log.fail('You are not login yet?'); 83 | // third parties 84 | } else if (argv.github || argv.linkedin) { 85 | // add future third parties here 86 | const functionMap = new Map( 87 | [ 88 | ['g', core.githubLogin], 89 | ['github', core.githubLogin], 90 | ['i', core.linkedinLogin], 91 | ['linkedin', core.linkedinLogin], 92 | ] 93 | ); 94 | const keyword = Object.entries(argv).filter((i) => (i[1] === true))[0][0]; 95 | const coreFunction = functionMap.get(keyword); 96 | prompt.colors = false; 97 | prompt.message = ''; 98 | prompt.start(); 99 | prompt.get([ 100 | {name: 'login', required: true}, 101 | {name: 'pass', required: true, hidden: true} 102 | ], function(e, user) { 103 | if (e) return log.fail(e); 104 | coreFunction(user, function(e, user) { 105 | if (e) return log.fail(e); 106 | log.info('Successfully third party login as', chalk.yellow(user.name)); 107 | }); 108 | }); 109 | } else if (argv.cookie) { 110 | // session 111 | prompt.colors = false; 112 | prompt.message = ''; 113 | prompt.start(); 114 | prompt.get([ 115 | {name: 'login', required: true}, 116 | {name: 'cookie', required: true} 117 | ], function(e, user) { 118 | if (e) return log.fail(e); 119 | core.cookieLogin(user, function(e, user) { 120 | if (e) return log.fail(e); 121 | log.info('Successfully cookie login as', chalk.yellow(user.name)); 122 | }); 123 | }); 124 | } else { 125 | // show current user 126 | user = session.getUser(); 127 | if (user) { 128 | log.info(chalk.gray(sprintf(' %-9s %-20s %s', 'Premium', 'User', 'Host'))); 129 | log.info(chalk.gray('-'.repeat(60))); 130 | log.printf(' %s %-20s %s', 131 | h.prettyText('', user.paid || false), 132 | chalk.yellow(user.name), 133 | config.sys.urls.base); 134 | } else 135 | return log.fail('You are not login yet?'); 136 | } 137 | }; 138 | 139 | module.exports = cmd; 140 | -------------------------------------------------------------------------------- /lib/commands/version.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | 4 | var file = require('../file'); 5 | var chalk = require('../chalk'); 6 | var icon = require('../icon'); 7 | var log = require('../log'); 8 | var Plugin = require('../plugin'); 9 | var session = require('../session'); 10 | 11 | const cmd = { 12 | command: 'version', 13 | aliases: ['info', 'env'], 14 | desc: 'Show version info', 15 | builder: function(yargs) { 16 | return yargs 17 | .example(chalk.yellow('leetcode version'), 'Show version number') 18 | .example(chalk.yellow('leetcode version -v'), 'Show more details'); 19 | } 20 | }; 21 | 22 | function printLine(k, v) { 23 | log.printf('%-20s %s', k, v); 24 | } 25 | 26 | function getVersion() { 27 | let version = require('../../package.json').version; 28 | 29 | try { 30 | const commit = require('../../.env.json').commit.short; 31 | if (commit) version += '-' + commit; 32 | } catch (e) {} 33 | 34 | return version; 35 | } 36 | 37 | cmd.handler = function(argv) { 38 | session.argv = argv; 39 | const version = getVersion(); 40 | 41 | if (!log.isEnabled('DEBUG')) 42 | return log.info(version); 43 | 44 | const logo = [ 45 | ' _ _ _ ', 46 | '| | | | | | ', 47 | '| | ___ ___| |_ ___ ___ __| | ___ ', 48 | '| |/ _ \\/ _ \\ __|/ __|/ _ \\ / _` |/ _ \\', 49 | '| | __/ __/ |_ (__| (_) | (_| | __/', 50 | '|_|\\___|\\___|\\__|\\___|\\___/ \\__,_|\\___| CLI ' + chalk.green('v' + version) 51 | ].join('\n'); 52 | log.info(logo); 53 | 54 | const os = require('os'); 55 | const config = require('../config'); 56 | 57 | log.info('\n[Environment]'); 58 | printLine('Node', process.version); 59 | printLine('OS', os.platform() + ' ' + os.release()); 60 | printLine('Cache', file.cacheDir()); 61 | printLine('Config', file.configFile()); 62 | 63 | log.info('\n[Configuration]'); 64 | _.each(config.getAll(true), function(v, k) { 65 | if (k === 'plugins') return; 66 | printLine(k, JSON.stringify(v)); 67 | }); 68 | 69 | log.info('\n[Themes]'); 70 | printLine('Colors', Array.from(chalk.themes.keys())); 71 | printLine('Icons', Array.from(icon.themes.keys())); 72 | 73 | log.info('\n[Plugins]'); 74 | for (let p of Plugin.plugins) printLine(p.name, p.ver); 75 | }; 76 | 77 | module.exports = cmd; 78 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | var nconf = require('nconf'); 4 | 5 | var file = require('./file'); 6 | 7 | const DEFAULT_CONFIG = { 8 | // usually you don't wanna change those 9 | sys: { 10 | categories: [ 11 | 'algorithms', 12 | 'database', 13 | 'shell', 14 | 'concurrency' 15 | ], 16 | langs: [ 17 | 'bash', 18 | 'c', 19 | 'cpp', 20 | 'csharp', 21 | 'golang', 22 | 'java', 23 | 'javascript', 24 | 'kotlin', 25 | 'mysql', 26 | 'php', 27 | 'python', 28 | 'python3', 29 | 'ruby', 30 | 'rust', 31 | 'scala', 32 | 'swift', 33 | 'typescript' 34 | ], 35 | urls: { 36 | // base urls 37 | base: 'https://leetcode.com', 38 | graphql: 'https://leetcode.com/graphql', 39 | login: 'https://leetcode.com/accounts/login/', 40 | // third part login base urls. TODO facebook google 41 | github_login: 'https://leetcode.com/accounts/github/login/?next=%2F', 42 | facebook_login: 'https://leetcode.com/accounts/facebook/login/?next=%2F', 43 | linkedin_login: 'https://leetcode.com/accounts/linkedin_oauth2/login/?next=%2F', 44 | // redirect urls 45 | leetcode_redirect: 'https://leetcode.com/', 46 | github_tf_redirect: 'https://github.com/sessions/two-factor', 47 | // simulate login urls 48 | github_login_request: 'https://github.com/login', 49 | github_session_request: 'https://github.com/session', 50 | github_tf_session_request: 'https://github.com/sessions/two-factor', 51 | linkedin_login_request: 'https://www.linkedin.com/login', 52 | linkedin_session_request: 'https://www.linkedin.com/checkpoint/lg/login-submit', 53 | // questions urls 54 | problems: 'https://leetcode.com/api/problems/$category/', 55 | problem: 'https://leetcode.com/problems/$slug/description/', 56 | test: 'https://leetcode.com/problems/$slug/interpret_solution/', 57 | session: 'https://leetcode.com/session/', 58 | submit: 'https://leetcode.com/problems/$slug/submit/', 59 | submissions: 'https://leetcode.com/api/submissions/$slug', 60 | submission: 'https://leetcode.com/submissions/detail/$id/', 61 | verify: 'https://leetcode.com/submissions/detail/$id/check/', 62 | favorites: 'https://leetcode.com/list/api/questions', 63 | favorite_delete: 'https://leetcode.com/list/api/questions/$hash/$id', 64 | plugin: 'https://raw.githubusercontent.com/leetcode-tools/leetcode-cli/master/lib/plugins/$name.js' 65 | }, 66 | }, 67 | 68 | // but you will want change these 69 | autologin: { 70 | enable: false, 71 | retry: 2 72 | }, 73 | code: { 74 | editor: 'vim', 75 | lang: 'cpp' 76 | }, 77 | file: { 78 | show: '${fid}.${slug}', 79 | submission: '${fid}.${slug}.${sid}.${ac}' 80 | }, 81 | color: { 82 | enable: true, 83 | theme: 'default' 84 | }, 85 | icon: { 86 | theme: '' 87 | }, 88 | network: { 89 | concurrency: 10, 90 | delay: 1 91 | }, 92 | plugins: {} 93 | }; 94 | 95 | function Config() {} 96 | 97 | Config.prototype.init = function() { 98 | nconf.file('local', file.configFile()) 99 | .add('global', {type: 'literal', store: DEFAULT_CONFIG}) 100 | .defaults({}); 101 | 102 | const cfg = nconf.get(); 103 | nconf.remove('local'); 104 | nconf.remove('global'); 105 | 106 | // HACK: remove old style configs 107 | for (const x in cfg) { 108 | if (x === x.toUpperCase()) delete cfg[x]; 109 | } 110 | delete DEFAULT_CONFIG.type; 111 | delete cfg.type; 112 | 113 | _.extendOwn(this, cfg); 114 | }; 115 | 116 | Config.prototype.getAll = function(useronly) { 117 | const cfg = _.extendOwn({}, this); 118 | if (useronly) delete cfg.sys; 119 | return cfg; 120 | }; 121 | 122 | module.exports = new Config(); 123 | -------------------------------------------------------------------------------- /lib/core.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | 4 | var _ = require('underscore'); 5 | var cheerio = require('cheerio'); 6 | var he = require('he'); 7 | 8 | var log = require('./log'); 9 | var h = require('./helper'); 10 | var file = require('./file'); 11 | var Plugin = require('./plugin'); 12 | 13 | const core = new Plugin(99999999, 'core', '20170722', 'Plugins manager'); 14 | 15 | core.filters = { 16 | query: { 17 | alias: 'query', 18 | type: 'string', 19 | default: '', 20 | describe: [ 21 | 'Filter questions by condition:', 22 | 'Uppercase means negative', 23 | 'e = easy E = m+h', 24 | 'm = medium M = e+h', 25 | 'h = hard H = e+m', 26 | 'd = done D = not done', 27 | 'l = locked L = non locked', 28 | 's = starred S = not starred' 29 | ].join('\n') 30 | }, 31 | tag: { 32 | alias: 'tag', 33 | type: 'array', 34 | default: [], 35 | describe: 'Filter questions by tag' 36 | } 37 | }; 38 | 39 | function hasTag(o, tag) { 40 | return Array.isArray(o) && o.some(x => x.indexOf(tag.toLowerCase()) >= 0); 41 | } 42 | 43 | const isLevel = (x, q) => x.level[0].toLowerCase() === q.toLowerCase(); 44 | const isACed = x => x.state === 'ac'; 45 | const isLocked = x => x.locked; 46 | const isStarred = x => x.starred; 47 | 48 | const QUERY_HANDLERS = { 49 | e: isLevel, 50 | E: _.negate(isLevel), 51 | m: isLevel, 52 | M: _.negate(isLevel), 53 | h: isLevel, 54 | H: _.negate(isLevel), 55 | l: isLocked, 56 | L: _.negate(isLocked), 57 | d: isACed, 58 | D: _.negate(isACed), 59 | s: isStarred, 60 | S: _.negate(isStarred) 61 | }; 62 | 63 | core.filterProblems = function(opts, cb) { 64 | this.getProblems(!opts.dontTranslate, function(e, problems) { 65 | if (e) return cb(e); 66 | 67 | for (let q of (opts.query || '').split('')) { 68 | const f = QUERY_HANDLERS[q]; 69 | if (!f) continue; 70 | problems = problems.filter(x => f(x, q)); 71 | } 72 | 73 | for (let t of (opts.tag || [])) { 74 | problems = problems.filter(function(x) { 75 | return x.category === t || 76 | hasTag(x.companies, t) || 77 | hasTag(x.tags, t); 78 | }); 79 | } 80 | 81 | return cb(null, problems); 82 | }); 83 | }; 84 | 85 | core.getProblem = function(keyword, needTranslation, cb) { 86 | if (keyword.id) 87 | return core.next.getProblem(keyword, needTranslation, cb); 88 | 89 | this.getProblems(needTranslation, function(e, problems) { 90 | if (e) return cb(e); 91 | 92 | keyword = Number(keyword) || keyword; 93 | const metaFid = file.exist(keyword) ? Number(file.meta(keyword).id) : NaN; 94 | const problem = problems.find(function(x) { 95 | return x.fid + '' === keyword + '' || x.fid + '' === metaFid + '' || x.name === keyword || x.slug === keyword; 96 | }); 97 | if (!problem) return cb('Problem not found!'); 98 | core.next.getProblem(problem, needTranslation, cb); 99 | }); 100 | }; 101 | 102 | core.starProblem = function(problem, starred, cb) { 103 | if (problem.starred === starred) { 104 | log.debug('problem is already ' + (starred ? 'starred' : 'unstarred')); 105 | return cb(null, starred); 106 | } 107 | 108 | core.next.starProblem(problem, starred, cb); 109 | }; 110 | 111 | core.exportProblem = function(problem, opts) { 112 | const data = _.extend({}, problem); 113 | 114 | // unify format before rendering 115 | data.app = require('./config').app || 'leetcode'; 116 | if (!data.fid) data.fid = data.id; 117 | if (!data.lang) data.lang = opts.lang; 118 | data.code = (opts.code || data.code || '').replace(/\r\n/g, '\n'); 119 | data.comment = h.langToCommentStyle(data.lang); 120 | data.percent = data.percent.toFixed(2); 121 | data.testcase = util.inspect(data.testcase || ''); 122 | 123 | if (opts.tpl === 'detailed') { 124 | let desc = data.desc; 125 | // Replace with '^' as the power operator 126 | desc = desc.replace(/<\/sup>/gm, '').replace(//gm, '^'); 127 | desc = he.decode(cheerio.load(desc).root().text()); 128 | // NOTE: wordwrap internally uses '\n' as EOL, so here we have to 129 | // remove all '\r' in the raw string. 130 | desc = desc.replace(/\r\n/g, '\n').replace(/^ /mg, '⁠'); 131 | const wrap = require('wordwrap')(79 - data.comment.line.length); 132 | data.desc = wrap(desc).split('\n'); 133 | } 134 | 135 | return file.render(opts.tpl, data); 136 | }; 137 | 138 | module.exports = core; 139 | -------------------------------------------------------------------------------- /lib/file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var os = require('os'); 4 | var path = require('path'); 5 | 6 | var _ = require('underscore'); 7 | var mkdirp = require('mkdirp'); 8 | 9 | const file = {} 10 | 11 | file.init = function() { 12 | _.templateSettings = { 13 | evaluate: /\{\{(.+?)\}\}/g, 14 | interpolate: /\$\{(.+?)\}/g 15 | }; 16 | }; 17 | 18 | file.isWindows = function() { 19 | return process.platform === 'win32'; 20 | }; 21 | 22 | /// app dirs /// 23 | file.userHomeDir = function() { 24 | return process.env.HOME || process.env.USERPROFILE; 25 | }; 26 | 27 | file.homeDir = function() { 28 | return path.join(this.userHomeDir(), '.lc'); 29 | }; 30 | 31 | file.appDir = function() { 32 | const config = require('./config'); 33 | return path.join(this.homeDir(), config.app || 'leetcode'); 34 | }; 35 | 36 | file.cacheDir = function() { 37 | return path.join(this.appDir(), 'cache'); 38 | }; 39 | 40 | file.codeDir = function(dir) { 41 | return path.join(__dirname, '..', dir || ''); 42 | }; 43 | 44 | /// app files /// 45 | file.cacheFile = function(k) { 46 | return path.join(this.cacheDir(), k + '.json'); 47 | }; 48 | 49 | file.configFile = function() { 50 | return path.join(this.homeDir(), 'config.json'); 51 | }; 52 | 53 | file.pluginFile = function(name) { 54 | return path.join(this.codeDir('lib/plugins'), path.basename(name)); 55 | }; 56 | 57 | file.listCodeDir = function(dir) { 58 | dir = this.codeDir(dir); 59 | return this.list(dir).map(function(f) { 60 | const fullpath = path.join(dir, f); 61 | const ext = path.extname(f); 62 | const name = path.basename(f, ext); 63 | 64 | let data = null; 65 | switch (ext) { 66 | case '.js': data = require(fullpath); break; 67 | case '.json': data = JSON.parse(file.data(fullpath)); break; 68 | } 69 | return {name: name, data: data, file: f}; 70 | }); 71 | }; 72 | 73 | /// general dirs & files /// 74 | file.mkdir = function(fullpath) { 75 | if (fs.existsSync(fullpath)) return; 76 | mkdirp.sync(fullpath); 77 | }; 78 | 79 | file.exist = function(fullpath) { 80 | return fs.existsSync(fullpath); 81 | }; 82 | 83 | file.rm = function(fullpath) { 84 | return fs.unlinkSync(fullpath); 85 | }; 86 | 87 | file.mv = function(src, dst) { 88 | return fs.renameSync(src, dst); 89 | }; 90 | 91 | file.list = function(dir) { 92 | return fs.readdirSync(dir); 93 | }; 94 | 95 | file.stat = function(fullpath) { 96 | return fs.statSync(fullpath); 97 | }; 98 | 99 | file.write = function(fullpath, data) { 100 | return fs.writeFileSync(fullpath, data); 101 | }; 102 | 103 | file.name = function(fullpath) { 104 | return path.basename(fullpath, path.extname(fullpath)); 105 | }; 106 | 107 | file.data = function(fullpath) { 108 | return fs.existsSync(fullpath) ? fs.readFileSync(fullpath).toString() : null; 109 | }; 110 | 111 | file.codeData = function(fullpath) { 112 | const data = this.data(fullpath); 113 | 114 | if (data === null) { 115 | return null; 116 | } 117 | 118 | const lines = data.split(/\r\n|\n|\r/); 119 | const start = lines.findIndex(x => x.indexOf('@lc code=start') !== -1); 120 | const end = lines.findIndex(x => x.indexOf('@lc code=end') !== -1); 121 | 122 | if (start !== -1 && end !== -1 && start + 1 <= end) { 123 | return lines.slice(start + 1, end).join(os.EOL); 124 | } 125 | 126 | return data; 127 | }; 128 | 129 | /// templates & metadata /// 130 | file.render = function(tpl, data) { 131 | const tplfile = path.join(this.codeDir('templates'), tpl + '.tpl'); 132 | let result = _.template(this.data(tplfile).replace(/\r\n/g, '\n'))(data); 133 | 134 | if (this.isWindows()) { 135 | result = result.replace(/\n/g, '\r\n'); 136 | } else { 137 | result = result.replace(/\r\n/g, '\n'); 138 | } 139 | return result; 140 | }; 141 | 142 | file.fmt = function(format, data) { 143 | return _.template(format)(data); 144 | }; 145 | 146 | file.metaByName = function(filename) { 147 | const m = {}; 148 | 149 | // expect the 1st section in filename as id 150 | // e.g. 1.two-sum.cpp 151 | m.id = file.name(filename).split('.')[0]; 152 | 153 | // HACK: compatible with old ext 154 | if (filename.endsWith('.py3') || filename.endsWith('.python3.py')) 155 | m.lang = 'python3'; 156 | else 157 | m.lang = require('./helper').extToLang(filename); 158 | 159 | return m; 160 | }; 161 | 162 | file.meta = function(filename) { 163 | const m = {}; 164 | 165 | // first look into the file data 166 | const line = this.data(filename).split('\n') 167 | .find(x => x.indexOf(' @lc app=') >= 0) || ''; 168 | line.split(' ').forEach(function(x) { 169 | const v = x.split('='); 170 | if (v.length == 2) { 171 | m[v[0]] = v[1].trim(); 172 | } 173 | }); 174 | 175 | // otherwise, look into file name 176 | if (!m.id || !m.lang) { 177 | const olddata = this.metaByName(filename); 178 | m.id = m.id || olddata.id; 179 | m.lang = m.lang || olddata.lang; 180 | } 181 | 182 | return m; 183 | }; 184 | 185 | module.exports = file; 186 | -------------------------------------------------------------------------------- /lib/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | var ora = require('ora'); 4 | 5 | var file = require('./file'); 6 | 7 | const UNITS_SIZE = [ 8 | {unit: 'B', name: 'Bytes', count: 1024}, 9 | {unit: 'K', name: 'KBytes', count: 1024}, 10 | {unit: 'M', name: 'MBytes', count: 1024}, 11 | {unit: 'G', name: 'GBytes', count: -1} 12 | ]; 13 | 14 | const UNITS_TIME = [ 15 | {unit: 's', name: 'seconds', count: 60}, 16 | {unit: 'm', name: 'minutes', count: 60}, 17 | {unit: 'h', name: 'hours', count: 24}, 18 | {unit: 'd', name: 'days', count: 7}, 19 | {unit: 'w', name: 'weeks', count: 4}, 20 | {unit: 'm', name: 'months', count: 12}, 21 | {unit: 'y', name: 'years', count: -1} 22 | ]; 23 | 24 | function getUnit(units, v) { 25 | for (let i = 0; i < units.length; ++i) { 26 | if (units[i].count <= 0 || v < units[i].count) 27 | return [v, units[i]]; 28 | v /= units[i].count; 29 | } 30 | } 31 | 32 | const LANGS = [ 33 | {lang: 'bash', ext: '.sh', style: '#'}, 34 | {lang: 'c', ext: '.c', style: 'c'}, 35 | {lang: 'cpp', ext: '.cpp', style: 'c'}, 36 | {lang: 'csharp', ext: '.cs', style: 'c'}, 37 | {lang: 'golang', ext: '.go', style: 'c'}, 38 | {lang: 'java', ext: '.java', style: 'c'}, 39 | {lang: 'javascript', ext: '.js', style: 'c'}, 40 | {lang: 'kotlin', ext: '.kt', style: 'c'}, 41 | {lang: 'mysql', ext: '.sql', style: '--'}, 42 | {lang: 'php', ext: '.php', style: 'c'}, 43 | {lang: 'python', ext: '.py', style: '#'}, 44 | {lang: 'python3', ext: '.py', style: '#'}, 45 | {lang: 'ruby', ext: '.rb', style: '#'}, 46 | {lang: 'rust', ext: '.rs', style: 'c'}, 47 | {lang: 'scala', ext: '.scala', style: 'c'}, 48 | {lang: 'swift', ext: '.swift', style: 'c'}, 49 | {lang: 'typescript', ext: '.ts', style: 'c'} 50 | ]; 51 | 52 | const h = {}; 53 | 54 | h.KEYS = { 55 | user: '../user', 56 | stat: '../stat', 57 | plugins: '../../plugins', 58 | problems: 'problems', 59 | translation: 'translationConfig', 60 | problem: p => p.fid + '.' + p.slug + '.' + p.category 61 | }; 62 | 63 | h.prettyState = function(state) { 64 | switch (state) { 65 | case 'ac': return this.prettyText('', true); 66 | case 'notac': return this.prettyText('', false); 67 | default: return ' '; 68 | } 69 | }; 70 | 71 | h.prettyText = function(text, yesNo) { 72 | const chalk = require('./chalk'); 73 | const icon = require('./icon'); 74 | switch (yesNo) { 75 | case true: return chalk.green(icon.yes + text); 76 | case false: return chalk.red(icon.no + text); 77 | default: return text; 78 | } 79 | }; 80 | 81 | h.prettySize = function(n) { 82 | const res = getUnit(UNITS_SIZE, n); 83 | return res[0].toFixed(2) + res[1].unit; 84 | }; 85 | 86 | h.prettyTime = function(n) { 87 | const res = getUnit(UNITS_TIME, n); 88 | return res[0].toFixed(0) + ' ' + res[1].name; 89 | }; 90 | 91 | h.prettyLevel = function(level) { 92 | const chalk = require('./chalk'); 93 | switch (level.toLowerCase().trim()) { 94 | case 'easy': return chalk.green(level); 95 | case 'medium': return chalk.yellow(level); 96 | case 'hard': return chalk.red(level); 97 | default: return level; 98 | } 99 | }; 100 | 101 | h.levelToName = function(level) { 102 | switch (level) { 103 | case 1: return 'Easy'; 104 | case 2: return 'Medium'; 105 | case 3: return 'Hard'; 106 | default: return ' '; 107 | } 108 | }; 109 | 110 | h.statusToName = function(sc) { 111 | switch (sc) { 112 | case 10: return 'Accepted'; 113 | case 11: return 'Wrong Answer'; 114 | case 12: return 'Memory Limit Exceeded'; 115 | case 13: return 'Output Limit Exceeded'; 116 | case 14: return 'Time Limit Exceeded'; 117 | case 15: return 'Runtime Error'; 118 | case 16: return 'Internal Error'; 119 | case 20: return 'Compile Error'; 120 | case 21: return 'Unknown Error'; 121 | default: return 'Unknown'; 122 | } 123 | }; 124 | 125 | h.langToExt = function(lang) { 126 | const res = LANGS.find(x => x.lang === lang); 127 | return res ? res.ext : '.raw'; 128 | }; 129 | 130 | h.extToLang = function(fullpath) { 131 | const res = LANGS.find(x => fullpath.endsWith(x.ext)); 132 | return res ? res.lang : 'unknown'; 133 | }; 134 | 135 | h.langToCommentStyle = function(lang) { 136 | const res = LANGS.find(x => x.lang === lang); 137 | 138 | return (res && res.style === 'c') ? 139 | {start: '/*', line: ' *', end: ' */', singleLine: '//'} : 140 | {start: res.style, line: res.style, end: res.style, singleLine: res.style}; 141 | }; 142 | 143 | h.readStdin = function(cb) { 144 | const stdin = process.stdin; 145 | const bufs = []; 146 | 147 | console.log('NOTE: to finish the input, press ' + 148 | (file.isWindows() ? ' and ' : '')); 149 | 150 | stdin.on('readable', function() { 151 | const data = stdin.read(); 152 | if (data) { 153 | // windows doesn't treat ctrl-D as EOF 154 | if (file.isWindows() && data.toString() === '\x04\r\n') { 155 | stdin.emit('end'); 156 | } else { 157 | bufs.push(data); 158 | } 159 | } 160 | }); 161 | stdin.on('end', function() { 162 | cb(null, Buffer.concat(bufs).toString()); 163 | }); 164 | stdin.on('error', cb); 165 | }; 166 | 167 | h.getSetCookieValue = function(resp, key) { 168 | const cookies = resp.headers['set-cookie']; 169 | if (!cookies) return null; 170 | 171 | for (let i = 0; i < cookies.length; ++i) { 172 | const sections = cookies[i].split(';'); 173 | for (let j = 0; j < sections.length; ++j) { 174 | const kv = sections[j].trim().split('='); 175 | if (kv[0] === key) return kv[1]; 176 | } 177 | } 178 | return null; 179 | }; 180 | 181 | h.printSafeHTTP = function(msg) { 182 | return msg.replace(/(Cookie\s*:\s*)'.*?'/, '$1') 183 | .replace(/('X-CSRFToken'\s*:\s*)'.*?'/, '$1') 184 | .replace(/('set-cookie'\s*:\s*)\[.*?\]/, '$1'); 185 | }; 186 | 187 | h.spin = function(s) { 188 | return ora(require('./chalk').gray(s)).start(); 189 | }; 190 | 191 | const COLORS = { 192 | blue: {fg: 'white', bg: 'bgBlue'}, 193 | cyan: {fg: 'white', bg: 'bgCyan'}, 194 | gray: {fg: 'white', bg: 'bgGray'}, 195 | green: {fg: 'black', bg: 'bgGreen'}, 196 | magenta: {fg: 'white', bg: 'bgMagenta'}, 197 | red: {fg: 'white', bg: 'bgRed'}, 198 | yellow: {fg: 'black', bg: 'bgYellow'}, 199 | white: {fg: 'black', bg: 'bgWhite'} 200 | }; 201 | h.badge = function(s, color) { 202 | s = ' ' + s + ' '; 203 | if (color === 'random') 204 | color = _.chain(COLORS).keys().sample().value(); 205 | const c = COLORS[color || 'blue']; 206 | 207 | const chalk = require('./chalk'); 208 | return chalk[c.fg][c.bg](s); 209 | }; 210 | 211 | module.exports = h; 212 | -------------------------------------------------------------------------------- /lib/icon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | 4 | var file = require('./file'); 5 | 6 | const icons = { 7 | yes: '✔', 8 | no: '✘', 9 | like: '★', 10 | unlike: '☆', 11 | lock: '🔒', 12 | empty: ' ', 13 | ac: '▣', 14 | notac: '▤', 15 | none: '⬚', 16 | 17 | themes: new Map() 18 | }; 19 | 20 | icons.setTheme = function(name) { 21 | const defaultName = file.isWindows() ? 'win7' : 'default'; 22 | const theme = this.themes.get(name) || this.themes.get(defaultName) || {}; 23 | _.extendOwn(this, theme); 24 | }; 25 | 26 | icons.init = function() { 27 | for (let f of file.listCodeDir('icons')) 28 | icons.themes.set(f.name, f.data); 29 | }; 30 | 31 | module.exports = icons; 32 | -------------------------------------------------------------------------------- /lib/log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | 4 | var chalk = require('./chalk'); 5 | var sprintf = require('./sprintf'); 6 | 7 | const log = { 8 | output: _.bind(console.log, console), 9 | level: null, 10 | levels: new Map([ 11 | ['TRACE', {value: 0, color: 'gray'}], 12 | ['DEBUG', {value: 1, color: 'gray'}], 13 | ['INFO', {value: 2, color: ''}], 14 | ['WARN', {value: 3, color: 'yellow'}], 15 | ['ERROR', {value: 4, color: 'red'}] 16 | ]) 17 | }; 18 | 19 | log.setLevel = function(name) { 20 | this.level = this.levels.get(name) || this.levels.get('INFO'); 21 | }; 22 | 23 | log.isEnabled = function(name) { 24 | return this.level.value <= this.levels.get(name).value; 25 | }; 26 | 27 | log.fail = function(e) { 28 | let msg = sprintf('%s', (e.msg || e)); 29 | if (e.statusCode) { 30 | msg += sprintf(' [code=%s]', e.statusCode); 31 | } 32 | log.error(msg); 33 | }; 34 | 35 | log.fatal = function(e) { 36 | log.error(e); 37 | process.exit(1); 38 | }; 39 | 40 | log.printf = function() { 41 | log.info(sprintf.apply(null, Array.from(arguments))); 42 | }; 43 | 44 | log.init = function() { 45 | this.setLevel('INFO'); 46 | 47 | for (let name of this.levels.keys()) { 48 | log[name.toLowerCase()] = function() { 49 | const level = log.levels.get(name); 50 | if (log.level.value > level.value) return; 51 | 52 | const args = Array.from(arguments); 53 | if (name !== 'INFO') args.unshift('[' + name + ']'); 54 | 55 | let s = args.map(x => x.toString()).join(' '); 56 | if (level.color) s = chalk[level.color](s); 57 | 58 | this.output(s); 59 | }; 60 | } 61 | }; 62 | 63 | module.exports = log; 64 | -------------------------------------------------------------------------------- /lib/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var cp = require('child_process'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | 6 | var _ = require('underscore'); 7 | var request = require('request'); 8 | 9 | var h = require('./helper'); 10 | var file = require('./file'); 11 | var cache = require('./cache'); 12 | var config = require('./config'); 13 | var log = require('./log'); 14 | var Queue = require('./queue'); 15 | 16 | function Plugin(id, name, ver, desc, deps) { 17 | this.id = id; 18 | this.name = name; 19 | this.ver = ver || 'default'; 20 | this.desc = desc || ''; 21 | 22 | this.enabled = true; 23 | this.deleted = false; 24 | this.missing = (this.ver === 'missing'); 25 | this.builtin = (this.ver === 'default'); 26 | 27 | // only need deps for current platform 28 | this.deps = _.chain(deps || []) 29 | .filter(x => ! x.includes(':') || x.includes(':' + process.platform)) 30 | .map(x => x.split(':')[0]) 31 | .value(); 32 | } 33 | 34 | Plugin.prototype.init = function() { 35 | this.config = config.plugins[this.name] || {}; 36 | this.next = null; 37 | }; 38 | 39 | Plugin.prototype.setNext = function(next) { 40 | Object.setPrototypeOf(this, next); 41 | this.next = next; 42 | }; 43 | 44 | Plugin.prototype.delete = function() { 45 | if (!this.missing) { 46 | try { 47 | const fullpath = file.pluginFile(this.file); 48 | file.rm(fullpath); 49 | } catch(e) { 50 | return log.error(e.message); 51 | } 52 | } 53 | this.deleted = true; 54 | }; 55 | 56 | Plugin.prototype.save = function() { 57 | const stats = cache.get(h.KEYS.plugins) || {}; 58 | 59 | if (this.deleted) delete stats[this.name]; 60 | else if (this.missing) return; 61 | else stats[this.name] = this.enabled; 62 | 63 | cache.set(h.KEYS.plugins, stats); 64 | }; 65 | 66 | Plugin.prototype.install = function(cb) { 67 | if (this.deps.length === 0) return cb(); 68 | 69 | const cmd = 'npm install --save ' + this.deps.join(' '); 70 | log.debug(cmd); 71 | const spin = h.spin(cmd); 72 | cp.exec(cmd, {cwd: file.codeDir()}, function(e) { 73 | spin.stop(); 74 | return cb(e); 75 | }); 76 | }; 77 | 78 | Plugin.prototype.help = function() {}; 79 | 80 | Plugin.plugins = []; 81 | 82 | Plugin.init = function(head) { 83 | log.trace('initializing all plugins'); 84 | head = head || require('./core'); 85 | 86 | const stats = cache.get(h.KEYS.plugins) || {}; 87 | 88 | // 1. find installed plugins 89 | let installed = []; 90 | for (let f of file.listCodeDir('lib/plugins')) { 91 | const p = f.data; 92 | if (!p) continue; 93 | log.trace('found plugin: ' + p.name + '=' + p.ver); 94 | 95 | p.file = f.file; 96 | p.enabled = stats[p.name]; 97 | 98 | if (!(p.name in stats)) { 99 | if (p.builtin) { 100 | log.trace('new builtin plugin, enable by default'); 101 | p.enabled = true; 102 | } else { 103 | log.trace('new 3rd party plugin, disable by default'); 104 | p.enabled = false; 105 | } 106 | } 107 | installed.push(p); 108 | } 109 | // the one with bigger `id` comes first 110 | installed = _.sortBy(installed, x => -x.id); 111 | 112 | // 2. init all in reversed order 113 | for (let i = installed.length - 1; i >= 0; --i) { 114 | const p = installed[i]; 115 | if (p.enabled) { 116 | p.init(); 117 | log.trace('inited plugin: ' + p.name); 118 | } else { 119 | log.trace('skipped plugin: ' + p.name); 120 | } 121 | } 122 | 123 | // 3. chain together 124 | const plugins = installed.filter(x => x.enabled); 125 | let last = head; 126 | for (let p of plugins) { 127 | last.setNext(p); 128 | last = p; 129 | } 130 | 131 | // 4. check missing plugins 132 | const missings = []; 133 | for (let k of _.keys(stats)) { 134 | if (installed.find(x => x.name === k)) continue; 135 | const p = new Plugin(-1, k, 'missing'); 136 | p.enabled = stats[k]; 137 | missings.push(p); 138 | log.trace('missing plugin:' + p.name); 139 | } 140 | 141 | Plugin.plugins = installed.concat(missings); 142 | return missings.length === 0; 143 | }; 144 | 145 | Plugin.copy = function(src, cb) { 146 | // FIXME: remove local file support? 147 | if (path.extname(src) !== '.js') { 148 | src = config.sys.urls.plugin.replace('$name', src); 149 | } 150 | const dst = file.pluginFile(src); 151 | 152 | const srcstream = src.startsWith('https://') ? request(src) : fs.createReadStream(src); 153 | const dststream = fs.createWriteStream(dst); 154 | let error; 155 | 156 | srcstream.on('response', function(resp) { 157 | if (resp.statusCode !== 200) 158 | srcstream.emit('error', 'HTTP Error: ' + resp.statusCode); 159 | }); 160 | srcstream.on('error', function(e) { 161 | dststream.emit('error', e); 162 | }); 163 | 164 | dststream.on('error', function(e) { 165 | error = e; 166 | dststream.end(); 167 | }); 168 | dststream.on('close', function() { 169 | spin.stop(); 170 | if (error) file.rm(dst); 171 | return cb(error, dst); 172 | }); 173 | 174 | log.debug('copying from ' + src); 175 | const spin = h.spin('Downloading ' + src); 176 | srcstream.pipe(dststream); 177 | }; 178 | 179 | Plugin.install = function(name, cb) { 180 | Plugin.copy(name, function(e, fullpath) { 181 | if (e) return cb(e); 182 | log.debug('copied to ' + fullpath); 183 | 184 | const p = require(fullpath); 185 | p.file = path.basename(fullpath); 186 | p.install(function() { 187 | return cb(null, p); 188 | }); 189 | }); 190 | }; 191 | 192 | Plugin.installMissings = function(cb) { 193 | function doTask(plugin, queue, cb) { 194 | Plugin.install(plugin.name, function(e, p) { 195 | if (!e) { 196 | p.enabled = plugin.enabled; 197 | p.save(); 198 | p.help(); 199 | } 200 | return cb(e, p); 201 | }); 202 | } 203 | 204 | const missings = Plugin.plugins.filter(x => x.missing); 205 | if (missings.length === 0) return cb(); 206 | 207 | log.warn('Installing missing plugins, might take a while ...'); 208 | const q = new Queue(missings, {}, doTask); 209 | q.run(1, cb); 210 | }; 211 | 212 | Plugin.save = function() { 213 | for (let p of this.plugins) p.save(); 214 | }; 215 | 216 | module.exports = Plugin; 217 | -------------------------------------------------------------------------------- /lib/plugins/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('underscore'); 3 | 4 | var cache = require('../cache'); 5 | var h = require('../helper'); 6 | var log = require('../log'); 7 | var Plugin = require('../plugin'); 8 | var session = require('../session'); 9 | 10 | const plugin = new Plugin(50, 'cache', '', 'Plugin to provide local cache.'); 11 | 12 | // this function will clear all caches if needTranslation is different than stored 13 | // it will also store the new needTranslation into cache automatically 14 | function clearCacheIfTchanged(needTranslation) { 15 | const translationConfig = cache.get(h.KEYS.translation); 16 | if (!translationConfig || translationConfig['useEndpointTranslation'] != needTranslation) { 17 | // cache doesn't have the key => old cache version, need to update 18 | // or cache does have the key but it contains a different value 19 | cache.deleteAll(); 20 | cache.set(h.KEYS.translation, { useEndpointTranslation: needTranslation }); 21 | log.debug('cache cleared: -T option changed'); 22 | } 23 | } 24 | 25 | plugin.getProblems = function (needTranslation, cb) { 26 | clearCacheIfTchanged(needTranslation); 27 | const problems = cache.get(h.KEYS.problems); 28 | if (problems) { 29 | log.debug('cache hit: problems.json'); 30 | return cb(null, problems); 31 | } 32 | 33 | plugin.next.getProblems(needTranslation, function(e, problems) { 34 | if (e) return cb(e); 35 | 36 | cache.set(h.KEYS.problems, problems); 37 | return cb(null, problems); 38 | }); 39 | }; 40 | 41 | plugin.getProblem = function (problem, needTranslation, cb) { 42 | clearCacheIfTchanged(needTranslation); 43 | const k = h.KEYS.problem(problem); 44 | const _problem = cache.get(k); 45 | if (_problem) { 46 | if (!_problem.desc.includes('
')) {
 47 |       // do not hit problem without html tags in desc (
 always exists for presenting testcase)
 48 |       log.debug('cache discarded for being no longer valid: ' + k + '.json');
 49 |     } else if (!['likes', 'dislikes'].every(p => p in _problem)) {
 50 |       // do not hit problem without likes & dislikes (logic will be improved in new lib)
 51 |       log.debug('cache discarded for being too old: ' + k + '.json');
 52 |     } else {
 53 |       // cache hit
 54 |       log.debug('cache hit: ' + k + '.json');
 55 |       _.extendOwn(problem, _problem);
 56 |       return cb(null, problem);
 57 |     }
 58 |   }
 59 | 
 60 |   plugin.next.getProblem(problem, needTranslation, function(e, _problem) {
 61 |     if (e) return cb(e);
 62 | 
 63 |     plugin.saveProblem(_problem);
 64 |     return cb(null, _problem);
 65 |   });
 66 | };
 67 | 
 68 | plugin.saveProblem = function(problem) {
 69 |   // it would be better to leave specific problem cache being user
 70 |   // independent, thus try to reuse existing cache as much as possible
 71 |   // after changing user.
 72 |   const _problem = _.omit(problem, ['locked', 'state', 'starred']);
 73 |   return cache.set(h.KEYS.problem(problem), _problem);
 74 | };
 75 | 
 76 | plugin.updateProblem = function(problem, kv) {
 77 |   const problems = cache.get(h.KEYS.problems);
 78 |   if (!problems) return false;
 79 | 
 80 |   const _problem = problems.find(x => x.id === problem.id);
 81 |   if (!_problem) return false;
 82 | 
 83 |   _.extend(_problem, kv);
 84 |   return cache.set(h.KEYS.problems, problems);
 85 | };
 86 | 
 87 | plugin.login = function(user, cb) {
 88 |   this.logout(user, false);
 89 |   plugin.next.login(user, function(e, user) {
 90 |     if (e) return cb(e);
 91 |     session.saveUser(user);
 92 |     return cb(null, user);
 93 |   });
 94 | };
 95 | 
 96 | plugin.logout = function(user, purge) {
 97 |   if (!user) user = session.getUser();
 98 |   if (purge) session.deleteUser();
 99 |   // NOTE: need invalidate any user related cache
100 |   session.deleteCodingSession();
101 |   return user;
102 | };
103 | 
104 | module.exports = plugin;
105 | 


--------------------------------------------------------------------------------
/lib/plugins/leetcode.cn.js:
--------------------------------------------------------------------------------
  1 | 'use strict';
  2 | var request = require('request');
  3 | 
  4 | var config = require('../config');
  5 | var h = require('../helper');
  6 | var log = require('../log');
  7 | var Plugin = require('../plugin');
  8 | var session = require('../session');
  9 | 
 10 | //
 11 | // [Usage]
 12 | //
 13 | // https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/leetcode.cn.md
 14 | //
 15 | var plugin = new Plugin(15, 'leetcode.cn', '2018.11.25',
 16 |     'Plugin to talk with leetcode-cn APIs.');
 17 | 
 18 | plugin.init = function() {
 19 |   config.app = 'leetcode.cn';
 20 |   config.sys.urls.base              = 'https://leetcode.cn';
 21 |   config.sys.urls.login             = 'https://leetcode.cn/accounts/login/';
 22 |   config.sys.urls.problems          = 'https://leetcode.cn/api/problems/$category/';
 23 |   config.sys.urls.problem           = 'https://leetcode.cn/problems/$slug/description/';
 24 |   config.sys.urls.graphql           = 'https://leetcode.cn/graphql';
 25 |   config.sys.urls.problem_detail    = 'https://leetcode.cn/graphql';
 26 |   config.sys.urls.test              = 'https://leetcode.cn/problems/$slug/interpret_solution/';
 27 |   config.sys.urls.session           = 'https://leetcode.cn/session/';
 28 |   config.sys.urls.submit            = 'https://leetcode.cn/problems/$slug/submit/';
 29 |   config.sys.urls.submissions       = 'https://leetcode.cn/api/submissions/$slug';
 30 |   config.sys.urls.submission        = 'https://leetcode.cn/submissions/detail/$id/';
 31 |   config.sys.urls.verify            = 'https://leetcode.cn/submissions/detail/$id/check/';
 32 |   config.sys.urls.favorites         = 'https://leetcode.cn/list/api/questions';
 33 |   config.sys.urls.favorite_delete   = 'https://leetcode.cn/list/api/questions/$hash/$id';
 34 |   // third parties
 35 |   config.sys.urls.github_login      = 'https://leetcode.cn/accounts/github/login/?next=%2F';
 36 |   config.sys.urls.linkedin_login    = 'https://leetcode.cn/accounts/linkedin_oauth2/login/?next=%2F';
 37 |   config.sys.urls.leetcode_redirect = 'https://leetcode.cn/';
 38 | };
 39 | 
 40 | // FIXME: refactor those
 41 | // update options with user credentials
 42 | function signOpts(opts, user) {
 43 |   opts.headers.Cookie = 'LEETCODE_SESSION=' + user.sessionId +
 44 |                         ';csrftoken=' + user.sessionCSRF + ';';
 45 |   opts.headers['X-CSRFToken'] = user.sessionCSRF;
 46 |   opts.headers['X-Requested-With'] = 'XMLHttpRequest';
 47 | }
 48 | 
 49 | function makeOpts(url) {
 50 |   const opts = {};
 51 |   opts.url = url;
 52 |   opts.headers = {};
 53 | 
 54 |   if (session.isLogin())
 55 |     signOpts(opts, session.getUser());
 56 |   return opts;
 57 | }
 58 | 
 59 | function checkError(e, resp, expectedStatus) {
 60 |   if (!e && resp && resp.statusCode !== expectedStatus) {
 61 |     const code = resp.statusCode;
 62 |     log.debug('http error: ' + code);
 63 | 
 64 |     if (code === 403 || code === 401) {
 65 |       e = session.errors.EXPIRED;
 66 |     } else {
 67 |       e = {msg: 'http error', statusCode: code};
 68 |     }
 69 |   }
 70 |   return e;
 71 | }
 72 | 
 73 | // overloading getProblems here to make sure everything related 
 74 | //   to listing out problems can have a chance to be translated. 
 75 | // NOTE: Details of the problem is translated inside leetcode.js
 76 | plugin.getProblems = function (needTranslation, cb) {
 77 |   plugin.next.getProblems(needTranslation, function(e, problems) {
 78 |     if (e) return cb(e);
 79 | 
 80 |     if (needTranslation) {
 81 |       // only translate titles of the list if user requested
 82 |       plugin.getProblemsTitle(function (e, titles) {
 83 |         if (e) return cb(e);
 84 | 
 85 |         problems.forEach(function (problem) {
 86 |           const title = titles[problem.id];
 87 |           if (title)
 88 |             problem.name = title;
 89 |         });
 90 | 
 91 |         return cb(null, problems);
 92 |       });
 93 |     } else {
 94 |       return cb(null, problems);
 95 |     }
 96 |   });
 97 | };
 98 | 
 99 | plugin.getProblemsTitle = function(cb) {
100 |   log.debug('running leetcode.cn.getProblemNames');
101 | 
102 |   const opts = makeOpts(config.sys.urls.graphql);
103 |   opts.headers.Origin = config.sys.urls.base;
104 |   opts.headers.Referer = 'https://leetcode.cn/api/problems/algorithms/';
105 | 
106 |   opts.json = true;
107 |   opts.body = {
108 |     query: [
109 |       'query getQuestionTranslation($lang: String) {',
110 |       '  translations: allAppliedQuestionTranslations(lang: $lang) {',
111 |       '    title',
112 |       '    questionId',
113 |       '    __typename',
114 |       '    }',
115 |       '}'
116 |     ].join('\n'),
117 |     variables:     {},
118 |     operationName: 'getQuestionTranslation'
119 |   };
120 | 
121 |   const spin = h.spin('Downloading questions titles');
122 |   request.post(opts, function(e, resp, body) {
123 |     spin.stop();
124 |     e = checkError(e, resp, 200);
125 |     if (e) return cb(e);
126 | 
127 |     const titles = [];
128 |     body.data.translations.forEach(function(x) {
129 |       titles[x.questionId] = x.title;
130 |     });
131 | 
132 |     return cb(null, titles);
133 |   });
134 | };
135 | 
136 | module.exports = plugin;
137 | 


--------------------------------------------------------------------------------
/lib/plugins/retry.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | var config = require('../config');
 3 | var log = require('../log');
 4 | var Plugin = require('../plugin');
 5 | var session = require('../session');
 6 | 
 7 | var plugin = new Plugin(30, 'retry', '',
 8 |     'Plugin to retry last failed request if autologin.enable is on.');
 9 | 
10 | const count = {};
11 | 
12 | function canRetry(e, name) {
13 |   return config.autologin.enable &&
14 |     (e === session.errors.EXPIRED) &&
15 |     (count[name] || 0) < config.autologin.retry;
16 | }
17 | 
18 | plugin.init = function() {
19 |   const names = [
20 |     'activateSession',
21 |     'createSession',
22 |     'deleteSession',
23 |     'getProblems',
24 |     'getProblem',
25 |     'getSessions',
26 |     'getSubmissions',
27 |     'getSubmission',
28 |     'getFavorites',
29 |     'testProblem',
30 |     'submitProblem',
31 |     'starProblem'
32 |   ];
33 | 
34 |   for (let name of names) {
35 |     count[name] = 0;
36 |     plugin[name] = function() {
37 |       const args = Array.from(arguments);
38 |       const cb = args.pop();
39 | 
40 |       const _cb = function() {
41 |         const results = Array.from(arguments);
42 |         const e = results[0];
43 |         if (!canRetry(e, name)) {
44 |           count[name] = 0;
45 |           return cb.apply(null, results);
46 |         }
47 | 
48 |         ++count[name];
49 |         plugin.relogin(function() {
50 |           // for now we don't care result, just blindly retry
51 |           plugin[name].apply(plugin, args.concat(cb));
52 |         });
53 |       };
54 | 
55 |       const next = plugin.next;
56 |       next[name].apply(next, args.concat(_cb));
57 |     };
58 |   }
59 | };
60 | 
61 | // leetcode.com is limiting one session alive in the same time,
62 | // which means once you login on web, your cli session will get
63 | // expired immediately. In that case we will try to re-login in
64 | // the backend to give a seamless user experience.
65 | plugin.relogin = function(cb) {
66 |   log.debug('session expired, try to re-login...');
67 | 
68 |   const user = session.getUser();
69 |   if (!user) {
70 |     log.debug('relogin failed: no user found, please login again');
71 |     return cb();
72 |   }
73 | 
74 |   this.login(user, function(e) {
75 |     if (e) {
76 |       log.debug('login failed:' + e.msg);
77 |     } else {
78 |       log.debug('login successfully, cont\'d...');
79 |     }
80 |     return cb();
81 |   });
82 | };
83 | 
84 | module.exports = plugin;
85 | 


--------------------------------------------------------------------------------
/lib/plugins/solution.discuss.js:
--------------------------------------------------------------------------------
  1 | var request = require('request');
  2 | 
  3 | var log = require('../log');
  4 | var chalk = require('../chalk');
  5 | var Plugin = require('../plugin');
  6 | var session = require('../session');
  7 | 
  8 | //
  9 | // [Usage]
 10 | //
 11 | // https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/solution.discuss.md
 12 | //
 13 | var plugin = new Plugin(200, 'solution.discuss', '2019.02.03',
 14 |     'Plugin to fetch most voted solution in discussions.');
 15 | 
 16 | var URL_DISCUSSES = 'https://leetcode.com/graphql';
 17 | var URL_DISCUSS = 'https://leetcode.com/problems/$slug/discuss/$id';
 18 | 
 19 | function getSolution(problem, lang, cb) {
 20 |   if (!problem) return cb();
 21 | 
 22 |   if (lang === 'python3') lang = 'python';
 23 | 
 24 |   var opts = {
 25 |     url:  URL_DISCUSSES,
 26 |     json: true,
 27 |     body: {
 28 |       query: [
 29 |         'query questionTopicsList($questionId: String!, $orderBy: TopicSortingOption, $skip: Int, $query: String, $first: Int!, $tags: [String!]) {',
 30 |         '  questionTopicsList(questionId: $questionId, orderBy: $orderBy, skip: $skip, query: $query, first: $first, tags: $tags) {',
 31 |         '    ...TopicsList',
 32 |         '  }',
 33 |         '}',
 34 |         'fragment TopicsList on TopicConnection {',
 35 |         '  totalNum',
 36 |         '  edges {',
 37 |         '    node {',
 38 |         '      id',
 39 |         '      title',
 40 |         '      post {',
 41 |         '        content',
 42 |         '        voteCount',
 43 |         '        author {',
 44 |         '          username',
 45 |         '        }',
 46 |         '      }',
 47 |         '    }',
 48 |         '  }',
 49 |         '}'
 50 |       ].join('\n'),
 51 | 
 52 |       operationName: 'questionTopicsList',
 53 |       variables:     JSON.stringify({
 54 |         query:      '',
 55 |         first:      1,
 56 |         skip:       0,
 57 |         orderBy:    'most_votes',
 58 |         questionId: '' + problem.id,
 59 |         tags:       [lang]
 60 |       })
 61 |     }
 62 |   };
 63 |   request(opts, function(e, resp, body) {
 64 |     if (e) return cb(e);
 65 |     if (resp.statusCode !== 200)
 66 |       return cb({msg: 'http error', statusCode: resp.statusCode});
 67 | 
 68 |     const solutions = body.data.questionTopicsList.edges;
 69 |     const solution = solutions.length > 0 ? solutions[0].node : null;
 70 |     return cb(null, solution);
 71 |   });
 72 | }
 73 | 
 74 | plugin.getProblem = function(problem, needTranslation, cb) {
 75 |   plugin.next.getProblem(problem, needTranslation, function(e, problem) {
 76 |     if (e || !session.argv.solution) return cb(e, problem);
 77 | 
 78 |     var lang = session.argv.lang;
 79 |     getSolution(problem, lang, function(e, solution) {
 80 |       if (e) return cb(e);
 81 |       if (!solution) return log.error('Solution not found for ' + lang);
 82 | 
 83 |       var link = URL_DISCUSS.replace('$slug', problem.slug).replace('$id', solution.id);
 84 |       var content = solution.post.content.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
 85 | 
 86 |       log.info();
 87 |       log.info(problem.name);
 88 |       log.info();
 89 |       log.info(solution.title);
 90 |       log.info();
 91 |       log.info(chalk.underline(link));
 92 |       log.info();
 93 |       log.info('* Lang:    ' + lang);
 94 |       log.info('* Author:  ' + solution.post.author.username);
 95 |       log.info('* Votes:   ' + solution.post.voteCount);
 96 |       log.info();
 97 |       log.info(content);
 98 |     });
 99 |   });
100 | };
101 | 
102 | module.exports = plugin;
103 | 


--------------------------------------------------------------------------------
/lib/queue.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | var _ = require('underscore');
 3 | 
 4 | var config = require('./config');
 5 | 
 6 | function Queue(tasks, ctx, onTask) {
 7 |   this.tasks = _.clone(tasks) || [];
 8 |   this.ctx = ctx || {};
 9 |   this.onTask = onTask;
10 |   this.error = null;
11 | }
12 | 
13 | Queue.prototype.addTask = function(task) {
14 |   this.tasks.push(task);
15 |   return this;
16 | };
17 | 
18 | Queue.prototype.addTasks = function(tasks) {
19 |   this.tasks = this.tasks.concat(tasks);
20 |   return this;
21 | };
22 | 
23 | Queue.prototype.run = function(concurrency, onDone) {
24 |   this.concurrency = concurrency || config.network.concurrency || 1;
25 |   this.onDone = onDone;
26 | 
27 |   const self = this;
28 |   for (let i = 0; i < this.concurrency; ++i) {
29 |     setImmediate(function() { self.workerRun(); });
30 |   }
31 | };
32 | 
33 | Queue.prototype.workerRun = function() {
34 |   // no more tasks, quit now
35 |   if (this.tasks.length === 0) {
36 |     if (--this.concurrency === 0 && this.onDone)
37 |       this.onDone(this.error, this.ctx);
38 |     return;
39 |   }
40 | 
41 |   const task = this.tasks.shift();
42 |   const self = this;
43 |   this.onTask(task, self, function(e) {
44 |     if (e) self.error = e;
45 | 
46 |     // TODO: could retry failed task here.
47 |     setImmediate(function() { self.workerRun(); });
48 |   });
49 | };
50 | 
51 | module.exports = Queue;
52 | 


--------------------------------------------------------------------------------
/lib/session.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | var moment = require('moment');
 3 | var _ = require('underscore');
 4 | 
 5 | var cache = require('./cache');
 6 | var config = require('./config');
 7 | var h = require('./helper');
 8 | 
 9 | const session = {};
10 | 
11 | session.errors = {
12 |   EXPIRED: {
13 |     msg:        'session expired, please login again',
14 |     statusCode: -1
15 |   }
16 | };
17 | 
18 | session.getUser = function() {
19 |   return cache.get(h.KEYS.user);
20 | };
21 | 
22 | session.saveUser = function(user) {
23 |   // when auto login enabled, have to save password to re-login later
24 |   // otherwise don't dump password for the sake of security.
25 |   const _user = _.omit(user, config.autologin.enable ? [] : ['pass']);
26 |   cache.set(h.KEYS.user, _user);
27 | };
28 | 
29 | session.deleteUser = function() {
30 |   cache.del(h.KEYS.user);
31 | };
32 | 
33 | session.deleteCodingSession = function() {
34 |   cache.del(h.KEYS.problems);
35 | };
36 | 
37 | session.isLogin = function() {
38 |   return this.getUser() !== null;
39 | };
40 | 
41 | session.updateStat = function(k, v) {
42 |   // TODO: use other storage if too many stat data
43 |   const today = moment().format('YYYY-MM-DD');
44 |   const stats = cache.get(h.KEYS.stat) || {};
45 |   const stat = stats[today] = stats[today] || {};
46 | 
47 |   if (k.endsWith('.set')) {
48 |     const s = new Set(stat[k] || []);
49 |     s.add(v);
50 |     stat[k] = Array.from(s);
51 |   } else {
52 |     stat[k] = (stat[k] || 0) + v;
53 |   }
54 | 
55 |   cache.set(h.KEYS.stat, stats);
56 | };
57 | 
58 | module.exports = session;
59 | 


--------------------------------------------------------------------------------
/lib/sprintf.js:
--------------------------------------------------------------------------------
 1 | 'use strict'
 2 | 
 3 | function len(s) {
 4 |   let s1 = s.replace(/\u001b\[[^m]*m/g, ''); // remove color controls
 5 |   s1 = s1.replace(/[^\x00-\xff]/g, '  '); // fix non-ascii
 6 |   return s1.length;
 7 | }
 8 | 
 9 | function padLeft(s, n, c) {
10 |   let k = Math.max(0, n - len(s));
11 |   return c.repeat(k) + s;
12 | }
13 | 
14 | function padRight(s, n , c) {
15 |   let k = Math.max(0, n - len(s));
16 |   return s + c.repeat(k);
17 | }
18 | 
19 | function padCenter(s, n, c) {
20 |   let k = Math.max(0, n - len(s));
21 |   let r = (k - k % 2) / 2, l = k - r;
22 |   return c.repeat(l) + s + c.repeat(r);
23 | }
24 | 
25 | const tsprintf = function() {
26 |   const args = Array.from(arguments);
27 |   let fmt = args.shift();
28 |   return fmt.replace(/%[^s%]*[s%]/g, function(s) {
29 |     if (s === '%%') return '%';
30 | 
31 |     let x = '' + args.shift();
32 |     let n = 0;
33 | 
34 |     s = s.slice(1, s.length-1);
35 |     if (s.length > 0) {
36 |       switch (s[0]) {
37 |         case '-':
38 |           n = parseInt(s.slice(1)) || 0;
39 |           x = padRight(x, n, ' ');
40 |           break;
41 |         case '=':
42 |           n = parseInt(s.slice(1)) || 0;
43 |           x = padCenter(x, n, ' ');
44 |           break;
45 |         case '0':
46 |           n = parseInt(s.slice(1)) || 0;
47 |           x = padLeft(x, n, '0');
48 |           break;
49 |         default:
50 |           n = parseInt(s) || 0;
51 |           x = padLeft(x, n, ' ');
52 |           break;
53 |       }
54 |     }
55 | 
56 |     return x;
57 |   });
58 | };
59 | 
60 | module.exports = tsprintf;
61 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "vsc-leetcode-cli",
 3 |   "version": "2.8.0",
 4 |   "description": "A cli tool to enjoy leetcode!",
 5 |   "engines": {
 6 |     "node": ">=4"
 7 |   },
 8 |   "bin": {
 9 |     "leetcode": "./bin/leetcode"
10 |   },
11 |   "scripts": {
12 |     "lint": "eslint lib/ test/",
13 |     "test": "npm run lint && nyc mocha test test/plugins && nyc report --reporter=lcov",
14 |     "travis": "node bin/pkg",
15 |     "pkg": "pkg . --out-path=dist/ --targets"
16 |   },
17 |   "pkg": {
18 |     "scripts": [
19 |       "lib"
20 |     ],
21 |     "assets": [
22 |       "colors",
23 |       "icons",
24 |       "templates"
25 |     ],
26 |     "targets": [
27 |       "node10-linux-x64",
28 |       "node10-macos-x64",
29 |       "node10-win-x64"
30 |     ]
31 |   },
32 |   "repository": {
33 |     "type": "git",
34 |     "url": "http://github.com/skygragon/leetcode-cli.git"
35 |   },
36 |   "keywords": [
37 |     "leetcode",
38 |     "cli",
39 |     "command",
40 |     "tool"
41 |   ],
42 |   "author": {
43 |     "name": "Eric Wang",
44 |     "email": "skygragon@gmail.com"
45 |   },
46 |   "license": "MIT",
47 |   "bugs": {
48 |     "url": "https://github.com/leetcode-tools/leetcode-cli/issues"
49 |   },
50 |   "homepage": "https://github.com/leetcode-tools/leetcode-cli#readme",
51 |   "dependencies": {
52 |     "ansi-styles": "3.2.1",
53 |     "cheerio": "0.20.0",
54 |     "he": "1.2.0",
55 |     "mkdirp": "^1.0.4",
56 |     "moment": "^2.29.1",
57 |     "nconf": "^0.11.2",
58 |     "ora": "3.0.0",
59 |     "prompt": "1.0.0",
60 |     "request": "2.88.0",
61 |     "supports-color": "5.5.0",
62 |     "underscore": "1.9.1",
63 |     "wordwrap": "1.0.0",
64 |     "yargs": "^15.4.1"
65 |   },
66 |   "devDependencies": {
67 |     "chai": "4.2.0",
68 |     "eslint": "5.9.0",
69 |     "eslint-config-google": "0.11.0",
70 |     "mocha": "^8.3.2",
71 |     "nock": "10.0.2",
72 |     "nyc": "^15.1.0",
73 |     "pkg": "^4.5.1",
74 |     "rewire": "4.0.1"
75 |   }
76 | }
77 | 


--------------------------------------------------------------------------------
/templates/codeonly.tpl:
--------------------------------------------------------------------------------
 1 | ${comment.start}
 2 | ${comment.line} @lc app=${app} id=${fid} lang=${lang}
 3 | ${comment.line}
 4 | ${comment.line} [${fid}] ${name}
 5 | ${comment.end}
 6 | 
 7 | ${comment.singleLine} @lc code=start
 8 | ${code}
 9 | ${comment.singleLine} @lc code=end
10 | 


--------------------------------------------------------------------------------
/templates/detailed.tpl:
--------------------------------------------------------------------------------
 1 | ${comment.start}
 2 | ${comment.line} @lc app=${app} id=${fid} lang=${lang}
 3 | ${comment.line}
 4 | ${comment.line} [${fid}] ${name}
 5 | ${comment.line}
 6 | ${comment.line} ${link}
 7 | ${comment.line}
 8 | ${comment.line} ${category}
 9 | ${comment.line} ${level} (${percent}%)
10 | ${comment.line} Likes:    ${likes}
11 | ${comment.line} Dislikes: ${dislikes}
12 | ${comment.line} Total Accepted:    ${totalAC}
13 | ${comment.line} Total Submissions: ${totalSubmit}
14 | ${comment.line} Testcase Example:  ${testcase}
15 | ${comment.line}
16 | {{ desc.forEach(function(x) { }}${comment.line} ${x}
17 | {{ }) }}${comment.end}
18 | 
19 | ${comment.singleLine} @lc code=start
20 | ${code}
21 | ${comment.singleLine} @lc code=end
22 | 


--------------------------------------------------------------------------------
/test/helper.js:
--------------------------------------------------------------------------------
 1 | 'use_strict';
 2 | const fs = require('fs');
 3 | 
 4 | const h = {
 5 |   DIR: './tmp/'
 6 | };
 7 | 
 8 | h.clean = function() {
 9 |   if (!fs.existsSync(this.DIR))
10 |     fs.mkdirSync(this.DIR);
11 |   for (let f of fs.readdirSync(this.DIR)) {
12 |     const fullpath = this.DIR + f;
13 |     if (fs.statSync(fullpath).isDirectory())
14 |       fs.rmdirSync(fullpath);
15 |     else
16 |       fs.unlinkSync(fullpath);
17 |   }
18 | };
19 | 
20 | module.exports = h;
21 | 


--------------------------------------------------------------------------------
/test/mock/add-two-numbers.20161015.json:
--------------------------------------------------------------------------------
1 | {"state":"ac","id":2,"category":"algorithms","name":"Add Two Numbers","key":"add-two-numbers","link":"https://leetcode.com/problems/add-two-numbers","locked":false,"percent":25.368142876074806,"level":"Medium","starred":true,"totalAC":"195263","totalSubmit":"769711","likes": "1","dislikes": "1","desc":"You are given two linked lists representing two non-negative numbers. The digits are stored in reverse order and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.\r\n\r\nInput: (2 -> 4 -> 3) + (5 -> 6 -> 4)\r\nOutput: 7 -> 0 -> 8","templates":[{"value":"cpp","text":"C++","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * struct ListNode {\r\n *     int val;\r\n *     ListNode *next;\r\n *     ListNode(int x) : val(x), next(NULL) {}\r\n * };\r\n */\r\nclass Solution {\r\npublic:\r\n    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {\r\n        \r\n    }\r\n};"},{"value":"java","text":"Java","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * public class ListNode {\r\n *     int val;\r\n *     ListNode next;\r\n *     ListNode(int x) { val = x; }\r\n * }\r\n */\r\npublic class Solution {\r\n    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {\r\n        \r\n    }\r\n}"},{"value":"python","text":"Python","defaultCode":"# Definition for singly-linked list.\r\n# class ListNode(object):\r\n#     def __init__(self, x):\r\n#         self.val = x\r\n#         self.next = None\r\n\r\nclass Solution(object):\r\n    def addTwoNumbers(self, l1, l2):\r\n        \"\"\"\r\n        :type l1: ListNode\r\n        :type l2: ListNode\r\n        :rtype: ListNode\r\n        \"\"\"\r\n        "},{"value":"c","text":"C","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * struct ListNode {\r\n *     int val;\r\n *     struct ListNode *next;\r\n * };\r\n */\r\nstruct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2) {\r\n    \r\n}"},{"value":"csharp","text":"C#","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * public class ListNode {\r\n *     public int val;\r\n *     public ListNode next;\r\n *     public ListNode(int x) { val = x; }\r\n * }\r\n */\r\npublic class Solution {\r\n    public ListNode AddTwoNumbers(ListNode l1, ListNode l2) {\r\n        \r\n    }\r\n}"},{"value":"javascript","text":"JavaScript","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * function ListNode(val) {\r\n *     this.val = val;\r\n *     this.next = null;\r\n * }\r\n */\r\n/**\r\n * @param {ListNode} l1\r\n * @param {ListNode} l2\r\n * @return {ListNode}\r\n */\r\nvar addTwoNumbers = function(l1, l2) {\r\n    \r\n};"},{"value":"ruby","text":"Ruby","defaultCode":"# Definition for singly-linked list.\r\n# class ListNode\r\n#     attr_accessor :val, :next\r\n#     def initialize(val)\r\n#         @val = val\r\n#         @next = nil\r\n#     end\r\n# end\r\n\r\n# @param {ListNode} l1\r\n# @param {ListNode} l2\r\n# @return {ListNode}\r\ndef add_two_numbers(l1, l2)\r\n    \r\nend"},{"value":"swift","text":"Swift","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * public class ListNode {\r\n *     public var val: Int\r\n *     public var next: ListNode?\r\n *     public init(_ val: Int) {\r\n *         self.val = val\r\n *         self.next = nil\r\n *     }\r\n * }\r\n */\r\nclass Solution {\r\n    func addTwoNumbers(_ l1: ListNode?, _ l2: ListNode?) -> ListNode? {\r\n        \r\n    }\r\n}"},{"value":"golang","text":"Go","defaultCode":"/**\r\n * Definition for singly-linked list.\r\n * type ListNode struct {\r\n *     Val int\r\n *     Next *ListNode\r\n * }\r\n */\r\nfunc addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {\r\n    \r\n}"}],"testcase":"[2,4,3]\n[5,6,4]","testable":true}
2 | 


--------------------------------------------------------------------------------
/test/mock/find-the-difference-star.json.20200821:
--------------------------------------------------------------------------------
1 | {"data":{"addQuestionToFavorite":{"ok":true,"error":null,"favoriteIdHash":"","questionId":"389","__typename":"AddQuestionToFavorite"}}}


--------------------------------------------------------------------------------
/test/mock/find-the-difference-unstar.json.20200821:
--------------------------------------------------------------------------------
1 | {"data":{"removeQuestionFromFavorite":{"ok":true,"error":null,"favoriteIdHash":"","questionId":"389","__typename":"RemoveQuestionFromFavorite"}}}


--------------------------------------------------------------------------------
/test/mock/find-the-difference.json.20171216:
--------------------------------------------------------------------------------
1 | {"data":{"question":{"content":"

\r\nGiven two strings s and t which consist of only lowercase letters.

\r\n\r\n

String t is generated by random shuffling string s and then add one more letter at a random position.

\r\n\r\n

Find the letter that was added in t.

\r\n\r\n

Example:\r\n

\r\nInput:\r\ns = \"abcd\"\r\nt = \"abcde\"\r\n\r\nOutput:\r\ne\r\n\r\nExplanation:\r\n'e' is the letter that was added.\r\n
","stats":"{\"totalAccepted\": \"89.7K\", \"totalSubmission\": \"175.7K\"}","codeDefinition":"[{\"text\": \"C++\", \"value\": \"cpp\", \"defaultCode\": \"class Solution {\\r\\npublic:\\r\\n char findTheDifference(string s, string t) {\\r\\n \\r\\n }\\r\\n};\"}, {\"text\": \"Java\", \"value\": \"java\", \"defaultCode\": \"class Solution {\\r\\n public char findTheDifference(String s, String t) {\\r\\n \\r\\n }\\r\\n}\"}, {\"text\": \"Python\", \"value\": \"python\", \"defaultCode\": \"class Solution(object):\\r\\n def findTheDifference(self, s, t):\\r\\n \\\"\\\"\\\"\\r\\n :type s: str\\r\\n :type t: str\\r\\n :rtype: str\\r\\n \\\"\\\"\\\"\\r\\n \"}, {\"text\": \"Python3\", \"value\": \"python3\", \"defaultCode\": \"class Solution:\\r\\n def findTheDifference(self, s, t):\\r\\n \\\"\\\"\\\"\\r\\n :type s: str\\r\\n :type t: str\\r\\n :rtype: str\\r\\n \\\"\\\"\\\"\\r\\n \"}, {\"text\": \"C\", \"value\": \"c\", \"defaultCode\": \"char findTheDifference(char* s, char* t) {\\r\\n \\r\\n}\"}, {\"text\": \"C#\", \"value\": \"csharp\", \"defaultCode\": \"public class Solution {\\r\\n public char FindTheDifference(string s, string t) {\\r\\n \\r\\n }\\r\\n}\"}, {\"text\": \"JavaScript\", \"value\": \"javascript\", \"defaultCode\": \"/**\\r\\n * @param {string} s\\r\\n * @param {string} t\\r\\n * @return {character}\\r\\n */\\r\\nvar findTheDifference = function(s, t) {\\r\\n \\r\\n};\"}, {\"text\": \"Ruby\", \"value\": \"ruby\", \"defaultCode\": \"# @param {String} s\\r\\n# @param {String} t\\r\\n# @return {Character}\\r\\ndef find_the_difference(s, t)\\r\\n \\r\\nend\"}, {\"text\": \"Swift\", \"value\": \"swift\", \"defaultCode\": \"class Solution {\\r\\n func findTheDifference(_ s: String, _ t: String) -> Character {\\r\\n \\r\\n }\\r\\n}\"}, {\"text\": \"Go\", \"value\": \"golang\", \"defaultCode\": \"func findTheDifference(s string, t string) byte {\\r\\n \\r\\n}\"}, {\"text\": \"Scala\", \"value\": \"scala\", \"defaultCode\": \"object Solution {\\n def findTheDifference(s: String, t: String): Char = {\\n \\n }\\n}\"}, {\"text\": \"Kotlin\", \"value\": \"kotlin\", \"defaultCode\": \"class Solution {\\n fun findTheDifference(s: String, t: String): Char {\\n \\n }\\n}\"}]","sampleTestCase":"\"abcd\"\n\"abcde\"","enableRunCode":true,"metaData":"{\r\n \"name\": \"findTheDifference\",\r\n \"params\": [\r\n {\r\n \"name\": \"s\",\r\n \"type\": \"string\"\r\n },\r\n {\r\n \"name\": \"t\",\r\n \"type\": \"string\"\r\n }\r\n ],\r\n \"return\": {\r\n \"type\": \"character\"\r\n }\r\n}","discussCategoryId":"511"}}} -------------------------------------------------------------------------------- /test/mock/two-sum.submissions.json.20170425: -------------------------------------------------------------------------------- 1 | {"has_next":true,"submissions_dump":[{"lang":"cpp","time":"1 month, 3 weeks","status_display":"Accepted","runtime":"12 ms","url":"/submissions/detail/95464136/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 1 week","status_display":"Accepted","runtime":"13 ms","url":"/submissions/detail/78502271/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Accepted","runtime":"9 ms","url":"/submissions/detail/77791021/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Compile Error","runtime":"N/A","url":"/submissions/detail/77790928/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Accepted","runtime":"13 ms","url":"/submissions/detail/77685402/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Wrong Answer","runtime":"N/A","url":"/submissions/detail/77685362/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Time Limit Exceeded","runtime":"N/A","url":"/submissions/detail/77685329/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Runtime Error","runtime":"N/A","url":"/submissions/detail/77685279/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Compile Error","runtime":"N/A","url":"/submissions/detail/77685195/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Wrong Answer","runtime":"N/A","url":"/submissions/detail/77685140/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Wrong Answer","runtime":"N/A","url":"/submissions/detail/77684623/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Compile Error","runtime":"N/A","url":"/submissions/detail/77684584/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Compile Error","runtime":"N/A","url":"/submissions/detail/77684436/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Time Limit Exceeded","runtime":"N/A","url":"/submissions/detail/77684406/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Runtime Error","runtime":"N/A","url":"/submissions/detail/77684353/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Runtime Error","runtime":"N/A","url":"/submissions/detail/77683773/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Runtime Error","runtime":"N/A","url":"/submissions/detail/77683680/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Runtime Error","runtime":"N/A","url":"/submissions/detail/77683411/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Compile Error","runtime":"N/A","url":"/submissions/detail/77683347/","is_pending":false,"title":"Two Sum"},{"lang":"cpp","time":"6 months, 2 weeks","status_display":"Time Limit Exceeded","runtime":"N/A","url":"/submissions/detail/77682978/","is_pending":false,"title":"Two Sum"}]} 2 | -------------------------------------------------------------------------------- /test/plugins/test_cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const _ = require('underscore'); 3 | const assert = require('chai').assert; 4 | const rewire = require('rewire'); 5 | 6 | const h = require('../../lib/helper'); 7 | const log = require('../../lib/log'); 8 | const config = require('../../lib/config'); 9 | const th = require('../helper'); 10 | 11 | describe('plugin:cache', function() { 12 | let plugin; 13 | let next; 14 | let cache; 15 | let file; 16 | let session; 17 | 18 | const PROBLEMS = [ 19 | {id: 0, fid: 0, name: 'name0', slug: 'slug0', starred: false, desc: '
', likes: '1', dislikes: '1', category: 'algorithms'},
 20 |     {id: 1, fid: 1, name: 'name1', slug: 'slug1', starred: true, desc: '
', likes: '1', dislikes: '1', category: 'algorithms'}
 21 |   ];
 22 |   const TRANSLATION_CONFIGS = { useEndpointTranslation: false };
 23 |   const PROBLEM = {id: 0, fid: 0, slug: 'slug0', category: 'algorithms'};
 24 | 
 25 |   before(function() {
 26 |     log.init();
 27 |     config.init();
 28 |   });
 29 | 
 30 |   beforeEach(function() {
 31 |     th.clean();
 32 |     next = {};
 33 | 
 34 |     file = rewire('../../lib/file');
 35 |     file.cacheDir = () => th.DIR;
 36 | 
 37 |     cache = rewire('../../lib/cache');
 38 |     cache.__set__('file', file);
 39 |     cache.init();
 40 | 
 41 |     session = rewire('../../lib/session');
 42 |     session.__set__('cache', cache);
 43 | 
 44 |     plugin = rewire('../../lib/plugins/cache');
 45 |     plugin.__set__('cache', cache);
 46 |     plugin.__set__('session', session);
 47 |     plugin.init();
 48 | 
 49 |     plugin.setNext(next);
 50 |   });
 51 | 
 52 |   describe('#getProblems', function() {
 53 |     it('should getProblems w/ cache ok', function(done) {
 54 |       cache.set('problems', PROBLEMS);
 55 |       cache.set(h.KEYS.translation, TRANSLATION_CONFIGS);
 56 | 
 57 |       plugin.getProblems(false, function(e, problems) {
 58 |         assert.equal(e, null);
 59 |         assert.deepEqual(problems, PROBLEMS);
 60 |         done();
 61 |       });
 62 |     });
 63 | 
 64 |     it('should getProblems w/o cache ok', function(done) {
 65 |       cache.del('problems');
 66 |       next.getProblems = (needT, cb) => cb(null, PROBLEMS);
 67 | 
 68 |       plugin.getProblems(false, function(e, problems) {
 69 |         assert.equal(e, null);
 70 |         assert.deepEqual(problems, PROBLEMS);
 71 |         done();
 72 |       });
 73 |     });
 74 | 
 75 |     it('should getProblems w/o cache fail if client error', function(done) {
 76 |       cache.del('problems');
 77 |       next.getProblems = (needT, cb) => cb('client getProblems error');
 78 | 
 79 |       plugin.getProblems(false, function(e, problems) {
 80 |         assert.equal(e, 'client getProblems error');
 81 |         done();
 82 |       });
 83 |     });
 84 |   }); // #getProblems
 85 | 
 86 |   describe('#getProblem', function() {
 87 |     it('should getProblem w/ cache ok', function(done) {
 88 |       cache.set('problems', PROBLEMS);
 89 |       cache.set(h.KEYS.translation, TRANSLATION_CONFIGS);
 90 |       cache.set('0.slug0.algorithms', PROBLEMS[0]);
 91 | 
 92 |       plugin.getProblem(_.clone(PROBLEM), false, function(e, problem) {
 93 |         assert.equal(e, null);
 94 |         assert.deepEqual(problem, PROBLEMS[0]);
 95 |         done();
 96 |       });
 97 |     });
 98 | 
 99 |     it('should getProblem w/o cache ok', function(done) {
100 |       cache.set('problems', PROBLEMS);
101 |       cache.del('0.slug0.algorithms');
102 |       next.getProblem = (problem, needT, cb) => cb(null, PROBLEMS[0]);
103 | 
104 |       plugin.getProblem(_.clone(PROBLEM), false, function(e, problem) {
105 |         assert.equal(e, null);
106 |         assert.deepEqual(problem, PROBLEMS[0]);
107 |         done();
108 |       });
109 |     });
110 | 
111 |     it('should getProblem fail if client error', function(done) {
112 |       cache.set('problems', PROBLEMS);
113 |       cache.del('0.slug0.algorithms');
114 |       next.getProblem = (problem, needT, cb) => cb('client getProblem error');
115 | 
116 |       plugin.getProblem(_.clone(PROBLEM), false, function(e, problem) {
117 |         assert.equal(e, 'client getProblem error');
118 |         done();
119 |       });
120 |     });
121 |   }); // #getProblem
122 | 
123 |   describe('#saveProblem', function() {
124 |     it('should ok', function() {
125 |       cache.del('0.slug0.algorithms');
126 | 
127 |       const problem = _.clone(PROBLEMS[0]);
128 |       problem.locked = true;
129 |       problem.state = 'ac';
130 | 
131 |       const ret = plugin.saveProblem(problem);
132 |       assert.equal(ret, true);
133 |       assert.deepEqual(cache.get('0.slug0.algorithms'),
134 |           {id: 0, fid: 0, slug: 'slug0', name: 'name0', desc: '
', likes: '1', dislikes: '1', category: 'algorithms'});
135 |     });
136 |   }); // #saveProblem
137 | 
138 |   describe('#updateProblem', function() {
139 |     it('should updateProblem ok', function(done) {
140 |       cache.set('problems', PROBLEMS);
141 |       cache.set(h.KEYS.translation, TRANSLATION_CONFIGS);
142 | 
143 |       const kv = {value: 'value00'};
144 |       const ret = plugin.updateProblem(PROBLEMS[0], kv);
145 |       assert.equal(ret, true);
146 | 
147 |       plugin.getProblems(false, function(e, problems) {
148 |         assert.equal(e, null);
149 |         assert.deepEqual(problems, [
150 |             {id: 0, fid: 0, name: 'name0', slug: 'slug0', value: 'value00', starred: false, desc: '
', likes: '1', dislikes: '1', category: 'algorithms'},
151 |             {id: 1, fid: 1, name: 'name1', slug: 'slug1', starred: true, desc: '
', likes: '1', dislikes: '1', category: 'algorithms'}
152 |         ]);
153 |         done();
154 |       });
155 |     });
156 | 
157 |     it('should updateProblem fail if no problems found', function() {
158 |       cache.del('problems');
159 |       const ret = plugin.updateProblem(PROBLEMS[0], {});
160 |       assert.equal(ret, false);
161 |     });
162 | 
163 |     it('should updateProblem fail if unknown problem', function() {
164 |       cache.set('problems', [PROBLEMS[1]]);
165 |       const ret = plugin.updateProblem(PROBLEMS[0], {});
166 |       assert.equal(ret, false);
167 |     });
168 |   }); // #updateProblem
169 | 
170 |   describe('#user', function() {
171 |     const USER = {name: 'test-user', pass: 'password'};
172 |     const USER_SAFE = {name: 'test-user'};
173 | 
174 |     it('should login ok', function(done) {
175 |       config.autologin.enable = true;
176 |       // before login
177 |       cache.del(h.KEYS.user);
178 |       assert.equal(session.getUser(), null);
179 |       assert.equal(session.isLogin(), false);
180 | 
181 |       next.login = (user, cb) => cb(null, user);
182 | 
183 |       plugin.login(USER, function(e, user) {
184 |         assert.equal(e, null);
185 |         assert.deepEqual(user, USER);
186 | 
187 |         // after login
188 |         assert.deepEqual(session.getUser(), USER);
189 |         assert.equal(session.isLogin(), true);
190 |         done();
191 |       });
192 |     });
193 | 
194 |     it('should login ok w/ auto login', function(done) {
195 |       config.autologin.enable = false;
196 |       cache.del(h.KEYS.user);
197 | 
198 |       next.login = (user, cb) => cb(null, user);
199 | 
200 |       plugin.login(USER, function(e, user) {
201 |         assert.equal(e, null);
202 |         assert.deepEqual(user, USER);
203 |         assert.deepEqual(session.getUser(), USER_SAFE);
204 |         assert.equal(session.isLogin(), true);
205 |         done();
206 |       });
207 |     });
208 | 
209 |     it('should login fail if client login error', function(done) {
210 |       next.login = (user, cb) => cb('client login error');
211 | 
212 |       plugin.login(USER, function(e, user) {
213 |         assert.equal(e, 'client login error');
214 |         done();
215 |       });
216 |     });
217 | 
218 |     it('should logout ok', function(done) {
219 |       // before logout
220 |       cache.set(h.KEYS.user, USER);
221 |       assert.deepEqual(session.getUser(), USER);
222 |       assert.equal(session.isLogin(), true);
223 | 
224 |       // after logout
225 |       plugin.logout(USER, true);
226 |       assert.equal(session.getUser(), null);
227 |       assert.equal(session.isLogin(), false);
228 |       done();
229 |     });
230 | 
231 |     it('should logout ok', function(done) {
232 |       // before logout
233 |       cache.set(h.KEYS.user, USER);
234 |       assert.deepEqual(session.getUser(), USER);
235 |       assert.equal(session.isLogin(), true);
236 | 
237 |       // after logout
238 |       plugin.logout(null, true);
239 |       assert.equal(session.getUser(), null);
240 |       assert.equal(session.isLogin(), false);
241 |       done();
242 |     });
243 |   }); // #user
244 | });
245 | 


--------------------------------------------------------------------------------
/test/plugins/test_retry.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | const assert = require('chai').assert;
 3 | const rewire = require('rewire');
 4 | 
 5 | const log = require('../../lib/log');
 6 | 
 7 | const config = rewire('../../lib/config');
 8 | const session = rewire('../../lib/session');
 9 | const plugin = rewire('../../lib/plugins/retry');
10 | 
11 | describe('plugin:retry', function() {
12 |   const USER = {};
13 |   const NEXT = {};
14 |   const PROBLEMS = [{id: 0, name: 'name0'}];
15 | 
16 |   before(function() {
17 |     log.init();
18 |     config.init();
19 |     plugin.init();
20 | 
21 |     session.getUser = () => USER;
22 | 
23 |     plugin.__set__('config', config);
24 |     plugin.__set__('session', session);
25 |     plugin.setNext(NEXT);
26 |   });
27 | 
28 |   it('should fail if auto login disabled', function(done) {
29 |     config.autologin.enable = false;
30 |     NEXT.getProblems = cb => cb(session.errors.EXPIRED);
31 | 
32 |     plugin.getProblems(function(e, problems) {
33 |       assert.equal(e, session.errors.EXPIRED);
34 |       done();
35 |     });
36 |   });
37 | 
38 |   it('should retry ok if finally ok', function(done) {
39 |     config.autologin.enable = true;
40 |     config.autologin.retry = 3;
41 | 
42 |     let n = 0;
43 |     NEXT.getProblems = function(cb) {
44 |       return ++n <= 3 ? cb(session.errors.EXPIRED) : cb(null, PROBLEMS);
45 |     };
46 |     NEXT.login = (user, cb) => cb(null, user);
47 | 
48 |     plugin.getProblems(function(e, problems) {
49 |       assert.notExists(e);
50 |       assert.equal(problems, PROBLEMS);
51 |       done();
52 |     });
53 |   });
54 | 
55 |   it('should retry fail if always failed', function(done) {
56 |     config.autologin.enable = true;
57 |     config.autologin.retry = 2;
58 | 
59 |     let n = 0;
60 |     NEXT.getProblems = function(cb) {
61 |       return ++n <= 3 ? cb(session.errors.EXPIRED) : cb(null, PROBLEMS);
62 |     };
63 |     NEXT.login = (user, cb) => {
64 |       return n == 1 ? cb(null, user) : cb('login failed');
65 |     }
66 | 
67 |     plugin.getProblems(function(e) {
68 |       assert.deepEqual(e, session.errors.EXPIRED);
69 |       done();
70 |     });
71 |   });
72 | 
73 |   it('should fail if user expired locally', function(done) {
74 |     config.autologin.enable = true;
75 | 
76 |     let n = 0;
77 |     NEXT.getProblems = function(cb) {
78 |       return ++n === 1 ? cb(session.errors.EXPIRED) : cb(null, PROBLEMS);
79 |     };
80 |     session.getUser = () => null;
81 | 
82 |     plugin.getProblems(function(e, problems) {
83 |       assert.notExists(e);
84 |       assert.equal(problems, PROBLEMS);
85 |       done();
86 |     });
87 |   });
88 | 
89 |   it('should fail if other errors', function(done) {
90 |     config.autologin.enable = true;
91 |     NEXT.getProblems = cb => cb('unknown error');
92 | 
93 |     plugin.getProblems(function(e, problems) {
94 |       assert.equal(e, 'unknown error');
95 |       done();
96 |     });
97 |   });
98 | });
99 | 


--------------------------------------------------------------------------------
/test/test_cache.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | const assert = require('chai').assert;
 3 | const rewire = require('rewire');
 4 | 
 5 | const th = require('./helper');
 6 | 
 7 | describe('cache', function() {
 8 |   let cache;
 9 | 
10 |   const K = '.test';
11 |   const V = {test: 'data'};
12 | 
13 |   beforeEach(function() {
14 |     th.clean();
15 | 
16 |     const file = rewire('../lib/file');
17 |     file.cacheDir = () => th.DIR;
18 | 
19 |     cache = rewire('../lib/cache');
20 |     cache.__set__('file', file);
21 |     cache.init();
22 |   });
23 | 
24 |   it('should get ok when not cached', function() {
25 |     cache.del(K);
26 |     assert.equal(cache.get(K), null);
27 |     assert.equal(cache.del(K), false);
28 |   });
29 | 
30 |   it('should get ok when cached', function() {
31 |     assert.equal(cache.set(K, V), true);
32 |     assert.deepEqual(cache.get(K), V);
33 |     assert.equal(cache.del(K), true);
34 |   });
35 | 
36 |   it('should list ok when no cached', function() {
37 |     const items = cache.list();
38 |     assert.equal(items.length, 0);
39 |   });
40 | 
41 |   it('should list ok when cached', function() {
42 |     assert.equal(cache.set(K, V), true);
43 |     const items = cache.list();
44 |     assert.equal(items.length, 1);
45 |     assert.equal(items[0].name, K);
46 |     assert.equal(items[0].size, JSON.stringify(V).length);
47 |   });
48 | });
49 | 


--------------------------------------------------------------------------------
/test/test_chalk.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | const assert = require('chai').assert;
 3 | const rewire = require('rewire');
 4 | 
 5 | // refer to https://en.wikipedia.org/wiki/ANSI_escape_code
 6 | describe('chalk', function() {
 7 |   let chalk;
 8 | 
 9 |   beforeEach(function() {
10 |     chalk = rewire('../lib/chalk');
11 |     chalk.enabled = true;
12 |     chalk.use256 = true;
13 |     chalk.use16m = false;
14 |   });
15 | 
16 |   it('should ok w/ 256 colors', function() {
17 |     chalk.init();
18 |     chalk.setTheme('default');
19 | 
20 |     assert.equal(chalk.black(' '),   '\u001b[38;5;16m \u001b[39m');
21 |     assert.equal(chalk.red(' '),     '\u001b[38;5;196m \u001b[39m');
22 |     assert.equal(chalk.green(' '),   '\u001b[38;5;46m \u001b[39m');
23 |     assert.equal(chalk.yellow(' '),  '\u001b[38;5;226m \u001b[39m');
24 |     assert.equal(chalk.blue(' '),    '\u001b[38;5;21m \u001b[39m');
25 |     assert.equal(chalk.magenta(' '), '\u001b[38;5;201m \u001b[39m');
26 |     assert.equal(chalk.cyan(' '),    '\u001b[38;5;51m \u001b[39m');
27 |     assert.equal(chalk.white(' '),   '\u001b[38;5;231m \u001b[39m');
28 | 
29 |     assert.equal(chalk.bold(' '),          '\u001b[1m \u001b[22m');
30 |     assert.equal(chalk.dim(' '),           '\u001b[2m \u001b[22m');
31 |     assert.equal(chalk.italic(' '),        '\u001b[3m \u001b[23m');
32 |     assert.equal(chalk.inverse(' '),       '\u001b[7m \u001b[27m');
33 |     assert.equal(chalk.strikethrough(' '), '\u001b[9m \u001b[29m');
34 |     assert.equal(chalk.underline(' '),     '\u001b[4m \u001b[24m');
35 |   });
36 | 
37 |   it('should ok w/ 8 colors', function() {
38 |     chalk.use256 = false;
39 |     chalk.init();
40 |     chalk.setTheme('default');
41 | 
42 |     assert.equal(chalk.black(' '),   '\u001b[30m \u001b[39m');
43 |     assert.equal(chalk.red(' '),     '\u001b[91m \u001b[39m');
44 |     assert.equal(chalk.green(' '),   '\u001b[92m \u001b[39m');
45 |     assert.equal(chalk.yellow(' '),  '\u001b[93m \u001b[39m');
46 |     assert.equal(chalk.blue(' '),    '\u001b[94m \u001b[39m');
47 |     assert.equal(chalk.magenta(' '), '\u001b[95m \u001b[39m');
48 |     assert.equal(chalk.cyan(' '),    '\u001b[96m \u001b[39m');
49 |     assert.equal(chalk.white(' '),   '\u001b[97m \u001b[39m');
50 |   });
51 | 
52 |   it('should ok w/o colors', function() {
53 |     chalk.enabled = false;
54 |     chalk.init();
55 |     chalk.setTheme('default');
56 | 
57 |     assert.equal(chalk.black(' '),   ' ');
58 |     assert.equal(chalk.red(' '),     ' ');
59 |     assert.equal(chalk.green(' '),   ' ');
60 |     assert.equal(chalk.yellow(' '),  ' ');
61 |     assert.equal(chalk.blue(' '),    ' ');
62 |     assert.equal(chalk.magenta(' '), ' ');
63 |     assert.equal(chalk.cyan(' '),    ' ');
64 |     assert.equal(chalk.white(' '),   ' ');
65 |   });
66 | 
67 |   it('should sprint w/ 256 colors ok', function() {
68 |     chalk.init();
69 |     chalk.setTheme('default');
70 |     assert.equal(chalk.sprint(' ', '#00ff00'), '\u001b[38;5;46m \u001b[39m');
71 |   });
72 | 
73 |   it('should sprint w/ 8 colors ok', function() {
74 |     chalk.use256 = false;
75 |     chalk.init();
76 |     chalk.setTheme('default');
77 |     assert.equal(chalk.sprint(' ', '#00ff00'), '\u001b[92m \u001b[39m');
78 |   });
79 | 
80 |   it('should set theme ok', function() {
81 |     chalk.init();
82 |     chalk.setTheme('dark');
83 |     assert.equal(chalk.sprint(' ', '#009900'), chalk.green(' '));
84 |   });
85 | 
86 |   it('should set unknown theme ok', function() {
87 |     chalk.init();
88 |     chalk.setTheme('unknown');
89 |     assert.equal(chalk.sprint(' ', '#00ff00'), chalk.green(' '));
90 |   });
91 | });
92 | 


--------------------------------------------------------------------------------
/test/test_config.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | const assert = require('chai').assert;
 3 | const rewire = require('rewire');
 4 | const _ = require('underscore');
 5 | 
 6 | const th = require('./helper');
 7 | 
 8 | describe('config', function() {
 9 |   let config;
10 |   const FILE = './tmp/config.json';
11 | 
12 |   beforeEach(function() {
13 |     th.clean();
14 | 
15 |     const file = rewire('../lib/file');
16 |     file.configFile = () => FILE;
17 | 
18 |     config = rewire('../lib/config');
19 |     config.__set__('file', file);
20 |   });
21 | 
22 |   function createConfigFile(data) {
23 |     const fs = require('fs');
24 |     fs.writeFileSync(FILE, JSON.stringify(data));
25 |   }
26 | 
27 |   it('should ok w/o local config', function() {
28 |     const DEFAULT_CONFIG = config.__get__('DEFAULT_CONFIG');
29 |     config.init();
30 | 
31 |     let actual = config.getAll();
32 |     let expect = DEFAULT_CONFIG;
33 |     assert.deepEqual(actual, expect);
34 | 
35 |     actual = config.getAll(true);
36 |     expect = _.omit(expect, 'sys');
37 |     assert.deepEqual(actual, expect);
38 |   });
39 | 
40 |   it('should ok w/ local config', function() {
41 |     createConfigFile({
42 |       autologin: {enable: false},
43 |       code:      {lang: 'ruby'},
44 |       color:     {enable: false}
45 |     });
46 |     config.init();
47 | 
48 |     assert.equal(config.autologin.enable, false);
49 |     assert.equal(config.code.lang, 'ruby');
50 |     assert.equal(config.color.enable, false);
51 |     assert.equal(config.code.editor, 'vim');
52 |   });
53 | 
54 |   it('should remove legacy keys', function() {
55 |     createConfigFile({
56 |       USE_COLOR: true,
57 |       code:      {lang: 'ruby'}
58 |     });
59 |     config.init();
60 | 
61 |     assert.equal(config.USE_COLOR, undefined);
62 |     assert.equal(config.code.lang, 'ruby');
63 |   });
64 | });
65 | 


--------------------------------------------------------------------------------
/test/test_core.js:
--------------------------------------------------------------------------------
  1 | 'use strict';
  2 | const assert = require('chai').assert;
  3 | const rewire = require('rewire');
  4 | 
  5 | describe('core', function() {
  6 |   let core;
  7 |   let next;
  8 | 
  9 |   const PROBLEMS = [
 10 |     {
 11 |       category: 'algorithms',
 12 |       id:       0,
 13 |       fid:      0,
 14 |       name:     'name0',
 15 |       slug:     'slug0',
 16 |       level:    'Hard',
 17 |       locked:   true,
 18 |       starred:  false,
 19 |       state:    'ac',
 20 |       tags:     ['google', 'facebook']
 21 |     },
 22 |     {
 23 |       category:  'algorithms',
 24 |       companies: ['amazon', 'facebook'],
 25 |       id:        1,
 26 |       fid:       1,
 27 |       name:      'name1',
 28 |       slug:      'slug1',
 29 |       level:     'Easy',
 30 |       locked:    false,
 31 |       starred:   true,
 32 |       state:     'none'
 33 |     }
 34 |   ];
 35 | 
 36 |   before(function() {
 37 |     const log = require('../lib/log');
 38 |     log.init();
 39 |   });
 40 | 
 41 |   beforeEach(function() {
 42 |     next = {};
 43 |     next.getProblems = (needTrans, cb) => cb(null, PROBLEMS);
 44 |     next.getProblem = (p, needTrans, cb) => cb(null, p);
 45 | 
 46 |     core = rewire('../lib/core');
 47 |     core.setNext(next);
 48 |   });
 49 | 
 50 |   describe('#filterProblems', function() {
 51 |     it('should filter by query ok', function(done) {
 52 |       const cases = [
 53 |         ['',     [0, 1]],
 54 |         ['x',    [0, 1]],
 55 |         ['h',    [0]],
 56 |         ['H',    [1]],
 57 |         ['m',    []],
 58 |         ['M',    [0, 1]],
 59 |         ['l',    [0]],
 60 |         ['L',    [1]],
 61 |         ['s',    [1]],
 62 |         ['S',    [0]],
 63 |         ['d',    [0]],
 64 |         ['D',    [1]],
 65 |         ['eLsD', [1]],
 66 |         ['Dh',   []]
 67 |       ];
 68 |       let n = cases.length;
 69 | 
 70 |       for (let x of cases) {
 71 |         core.filterProblems({query: x[0], dontTranslate: false}, function(e, problems) {
 72 |           assert.notExists(e);
 73 |           assert.equal(problems.length, x[1].length);
 74 | 
 75 |           for (let i = 0; i < problems.length; ++i)
 76 |             assert.equal(problems[i], PROBLEMS[x[1][i]]);
 77 |           if (--n === 0) done();
 78 |         });
 79 |       }
 80 |     });
 81 | 
 82 |     it('should filter by tag ok', function(done) {
 83 |       const cases = [
 84 |         [[],           [0, 1]],
 85 |         [['facebook'], [0, 1]],
 86 |         [['google'],   [0]],
 87 |         [['amazon'],   [1]],
 88 |         [['apple'],    []],
 89 |       ];
 90 |       let n = cases.length;
 91 | 
 92 |       for (let x of cases) {
 93 |         core.filterProblems({ tag: x[0], dontTranslate: false}, function(e, problems) {
 94 |           assert.notExists(e);
 95 |           assert.equal(problems.length, x[1].length);
 96 | 
 97 |           for (let i = 0; i < problems.length; ++i)
 98 |             assert.equal(problems[i], PROBLEMS[x[1][i]]);
 99 |           if (--n === 0) done();
100 |         });
101 |       }
102 |     });
103 | 
104 |     it('should fail if getProblems error', function(done) {
105 |       next.getProblems = (needT, cb) => cb('getProblems error');
106 |       core.filterProblems({}, function(e) {
107 |         assert.equal(e, 'getProblems error');
108 |         done();
109 |       });
110 |     });
111 |   }); // #filterProblems
112 | 
113 |   describe('#starProblem', function() {
114 |     it('should ok', function(done) {
115 |       next.starProblem = (p, starred, cb) => cb(null, starred);
116 | 
117 |       assert.equal(PROBLEMS[0].starred, false);
118 |       core.starProblem(PROBLEMS[0], true, function(e, starred) {
119 |         assert.notExists(e);
120 |         assert.equal(starred, true);
121 |         done();
122 |       });
123 |     });
124 | 
125 |     it('should ok if already starred', function(done) {
126 |       assert.equal(PROBLEMS[1].starred, true);
127 |       core.starProblem(PROBLEMS[1], true, function(e, starred) {
128 |         assert.notExists(e);
129 |         assert.equal(starred, true);
130 |         done();
131 |       });
132 |     });
133 | 
134 |     it('should ok if already unstarred', function(done) {
135 |       assert.equal(PROBLEMS[0].starred, false);
136 |       core.starProblem(PROBLEMS[0], false, function(e, starred) {
137 |         assert.notExists(e);
138 |         assert.equal(starred, false);
139 |         done();
140 |       });
141 |     });
142 |   }); // #starProblem
143 | 
144 |   describe('#exportProblem', function() {
145 |     let file;
146 | 
147 |     beforeEach(function() {
148 |       file = rewire('../lib/file');
149 |       file.init();
150 |       core.__set__('file', file);
151 |     });
152 | 
153 |     it('should codeonly ok', function() {
154 |       file.isWindows = () => false;
155 | 
156 |       const expected = [
157 |         '/*',
158 |         ' * @lc app=leetcode id=2 lang=cpp',
159 |         ' *',
160 |         ' * [2] Add Two Numbers',
161 |         ' */',
162 |         '',
163 |         '// @lc code=start',
164 |         '/**',
165 |         ' * Definition for singly-linked list.',
166 |         ' * struct ListNode {',
167 |         ' *     int val;',
168 |         ' *     ListNode *next;',
169 |         ' *     ListNode(int x) : val(x), next(NULL) {}',
170 |         ' * };',
171 |         ' */',
172 |         'class Solution {',
173 |         'public:',
174 |         '    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {',
175 |         '        ',
176 |         '    }',
177 |         '};',
178 |         '// @lc code=end',
179 |         ''
180 |       ].join('\n');
181 | 
182 |       const problem = require('./mock/add-two-numbers.20161015.json');
183 |       const opts = {
184 |         lang: 'cpp',
185 |         code: problem.templates[0].defaultCode,
186 |         tpl:  'codeonly'
187 |       };
188 |       assert.equal(core.exportProblem(problem, opts), expected);
189 |     });
190 | 
191 |     it('should codeonly ok in windows', function() {
192 |       file.isWindows = () => true;
193 | 
194 |       const expected = [
195 |         '/*',
196 |         ' * @lc app=leetcode id=2 lang=cpp',
197 |         ' *',
198 |         ' * [2] Add Two Numbers',
199 |         ' */',
200 |         '',
201 |         '// @lc code=start',
202 |         '/**',
203 |         ' * Definition for singly-linked list.',
204 |         ' * struct ListNode {',
205 |         ' *     int val;',
206 |         ' *     ListNode *next;',
207 |         ' *     ListNode(int x) : val(x), next(NULL) {}',
208 |         ' * };',
209 |         ' */',
210 |         'class Solution {',
211 |         'public:',
212 |         '    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {',
213 |         '        ',
214 |         '    }',
215 |         '};',
216 |         '// @lc code=end',
217 |         ''
218 |       ].join('\r\n');
219 | 
220 |       const problem = require('./mock/add-two-numbers.20161015.json');
221 |       const opts = {
222 |         lang: 'cpp',
223 |         code: problem.templates[0].defaultCode,
224 |         tpl:  'codeonly'
225 |       };
226 |       assert.equal(core.exportProblem(problem, opts), expected);
227 |     });
228 | 
229 |     it('should detailed ok with cpp', function() {
230 |       file.isWindows = () => false;
231 | 
232 |       const expected = [
233 |         '/*',
234 |         ' * @lc app=leetcode id=2 lang=cpp',
235 |         ' *',
236 |         ' * [2] Add Two Numbers',
237 |         ' *',
238 |         ' * https://leetcode.com/problems/add-two-numbers',
239 |         ' *',
240 |         ' * algorithms',
241 |         ' * Medium (25.37%)',
242 |         ' * Likes:    1',
243 |         ' * Dislikes: 1',
244 |         ' * Total Accepted:    195263',
245 |         ' * Total Submissions: 769711',
246 |         ' * Testcase Example:  \'[2,4,3]\\n[5,6,4]\'',
247 |         ' *',
248 |         ' * You are given two linked lists representing two non-negative numbers. The',
249 |         ' * digits are stored in reverse order and each of their nodes contain a single',
250 |         ' * digit. Add the two numbers and return it as a linked list.',
251 |         ' * ',
252 |         ' * Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)',
253 |         ' * Output: 7 -> 0 -> 8',
254 |         ' */',
255 |         '',
256 |         '// @lc code=start',
257 |         '/**',
258 |         ' * Definition for singly-linked list.',
259 |         ' * struct ListNode {',
260 |         ' *     int val;',
261 |         ' *     ListNode *next;',
262 |         ' *     ListNode(int x) : val(x), next(NULL) {}',
263 |         ' * };',
264 |         ' */',
265 |         'class Solution {',
266 |         'public:',
267 |         '    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {',
268 |         '        ',
269 |         '    }',
270 |         '};',
271 |         '// @lc code=end',
272 |         ''
273 |       ].join('\n');
274 | 
275 |       const problem = require('./mock/add-two-numbers.20161015.json');
276 |       const opts = {
277 |         lang: 'cpp',
278 |         code: problem.templates[0].defaultCode,
279 |         tpl:  'detailed'
280 |       };
281 |       assert.equal(core.exportProblem(problem, opts), expected);
282 |     });
283 | 
284 |     it('should detailed ok with ruby', function() {
285 |       file.isWindows = () => false;
286 | 
287 |       const expected = [
288 |         '#',
289 |         '# @lc app=leetcode id=2 lang=ruby',
290 |         '#',
291 |         '# [2] Add Two Numbers',
292 |         '#',
293 |         '# https://leetcode.com/problems/add-two-numbers',
294 |         '#',
295 |         '# algorithms',
296 |         '# Medium (25.37%)',
297 |         '# Likes:    1',
298 |         '# Dislikes: 1',
299 |         '# Total Accepted:    195263',
300 |         '# Total Submissions: 769711',
301 |         '# Testcase Example:  \'\'',
302 |         '#',
303 |         '# You are given two linked lists representing two non-negative numbers. The',
304 |         '# digits are stored in reverse order and each of their nodes contain a single',
305 |         '# digit. Add the two numbers and return it as a linked list.',
306 |         '# ',
307 |         '# Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)',
308 |         '# Output: 7 -> 0 -> 8',
309 |         '#',
310 |         '',
311 |         '# @lc code=start',
312 |         '# Definition for singly-linked list.',
313 |         '# class ListNode',
314 |         '#     attr_accessor :val, :next',
315 |         '#     def initialize(val)',
316 |         '#         @val = val',
317 |         '#         @next = nil',
318 |         '#     end',
319 |         '# end',
320 |         '',
321 |         '# @param {ListNode} l1',
322 |         '# @param {ListNode} l2',
323 |         '# @return {ListNode}',
324 |         'def add_two_numbers(l1, l2)',
325 |         '    ',
326 |         'end',
327 |         '# @lc code=end',
328 |         ''
329 |       ].join('\n');
330 | 
331 |       const problem = require('./mock/add-two-numbers.20161015.json');
332 |       problem.testcase = null;
333 |       const opts = {
334 |         lang: 'ruby',
335 |         code: problem.templates[6].defaultCode,
336 |         tpl:  'detailed'
337 |       };
338 |       assert.equal(core.exportProblem(problem, opts), expected);
339 |     });
340 |   }); // #exportProblem
341 | 
342 |   describe('#getProblem', function() {
343 |     it('should get by id ok', function (done) {
344 |       // set needTranslate to false here because it's not used anyways
345 |       core.getProblem(0, false, function(e, problem) {
346 |         assert.notExists(e);
347 |         assert.deepEqual(problem, PROBLEMS[0]);
348 |         done();
349 |       });
350 |     });
351 | 
352 |     it('should get by key ok', function(done) {
353 |       core.getProblem('slug0', false, function(e, problem) {
354 |         assert.notExists(e);
355 |         assert.deepEqual(problem, PROBLEMS[0]);
356 |         done();
357 |       });
358 |     });
359 | 
360 |     it('should fail if not found', function(done) {
361 |       core.getProblem(3, false, function(e, problem) {
362 |         assert.equal(e, 'Problem not found!');
363 |         done();
364 |       });
365 |     });
366 | 
367 |     it('should fail if client error', function(done) {
368 |       next.getProblem = (problem, needT, cb) => cb('client getProblem error');
369 | 
370 |       core.getProblem(0, false, function(e, problem) {
371 |         assert.equal(e, 'client getProblem error');
372 |         done();
373 |       });
374 |     });
375 | 
376 |     it('should ok if problem is already there', function(done) {
377 |       core.getProblem(PROBLEMS[1], false, function(e, problem) {
378 |         assert.notExists(e);
379 |         assert.deepEqual(problem, PROBLEMS[1]);
380 |         done();
381 |       });
382 |     });
383 | 
384 |     it('should fail if getProblems error', function(done) {
385 |       next.getProblems = (needT, cb) => cb('getProblems error');
386 | 
387 |       core.getProblem(0, false, function(e, problem) {
388 |         assert.equal(e, 'getProblems error');
389 |         done();
390 |       });
391 |     });
392 |   }); // #getProblem
393 | });
394 | 


--------------------------------------------------------------------------------
/test/test_file.js:
--------------------------------------------------------------------------------
  1 | 'use strict';
  2 | const fs = require('fs');
  3 | const path = require('path');
  4 | 
  5 | const assert = require('chai').assert;
  6 | const rewire = require('rewire');
  7 | 
  8 | const th = require('./helper');
  9 | 
 10 | describe('file', function() {
 11 |   let file;
 12 | 
 13 |   beforeEach(function() {
 14 |     file = rewire('../lib/file');
 15 |   });
 16 | 
 17 |   describe('#dirAndFiles', function() {
 18 |     const HOME = path.join(__dirname, '..');
 19 | 
 20 |     it('should ok on linux', function() {
 21 |       if (file.isWindows()) this.skip();
 22 |       process.env.HOME = '/home/skygragon';
 23 | 
 24 |       assert.equal(file.userHomeDir(), '/home/skygragon');
 25 |       assert.equal(file.homeDir(), '/home/skygragon/.lc');
 26 |       assert.equal(file.cacheDir(), '/home/skygragon/.lc/leetcode/cache');
 27 |       assert.equal(file.cacheFile('xxx'), '/home/skygragon/.lc/leetcode/cache/xxx.json');
 28 |       assert.equal(file.configFile(), '/home/skygragon/.lc/config.json');
 29 |       assert.equal(file.name('/home/skygragon/.lc/leetcode/cache/xxx.json'), 'xxx');
 30 |     });
 31 | 
 32 |     it('should ok on windows', function() {
 33 |       if (!file.isWindows()) this.skip();
 34 |       process.env.HOME = '';
 35 |       process.env.USERPROFILE = 'C:\\Users\\skygragon';
 36 |       assert.equal(file.userHomeDir(), 'C:\\Users\\skygragon');
 37 |       assert.equal(file.homeDir(), 'C:\\Users\\skygragon\\.lc');
 38 |       assert.equal(file.cacheDir(), 'C:\\Users\\skygragon\\.lc\\leetcode\\cache');
 39 |       assert.equal(file.cacheFile('xxx'), 'C:\\Users\\skygragon\\.lc\\leetcode\\cache\\xxx.json');
 40 |       assert.equal(file.configFile(), 'C:\\Users\\skygragon\\.lc\\config.json');
 41 |       assert.equal(file.name('C:\\Users\\skygragon\\.lc\\leetcode\\cache\\xxx.json'), 'xxx');
 42 |     });
 43 | 
 44 |     it('should codeDir ok', function() {
 45 |       assert.equal(file.codeDir(), HOME);
 46 |       assert.equal(file.codeDir('.'), HOME);
 47 |       assert.equal(file.codeDir('icons'), path.join(HOME, 'icons'));
 48 |       assert.equal(file.codeDir('lib/plugins'), path.join(HOME, 'lib', 'plugins'));
 49 |     });
 50 | 
 51 |     it('should listCodeDir ok', function() {
 52 |       const files = file.listCodeDir('lib/plugins');
 53 |       assert.equal(files.length, 6);
 54 |       assert.equal(files[0].name, 'cache');
 55 |       assert.equal(files[1].name, 'company');
 56 |       assert.equal(files[2].name, 'leetcode.cn');
 57 |       assert.equal(files[3].name, 'leetcode');
 58 |       assert.equal(files[4].name, 'retry');
 59 |       assert.equal(files[5].name, 'solution.discuss');
 60 |     });
 61 | 
 62 |     it('should pluginFile ok', function() {
 63 |       const expect = path.join(HOME, 'lib/plugins/cache.js');
 64 |       assert.equal(file.pluginFile('cache.js'), expect);
 65 |       assert.equal(file.pluginFile('./cache.js'), expect);
 66 |       assert.equal(file.pluginFile('https://github.com/skygragon/cache.js'), expect);
 67 |     });
 68 | 
 69 |     it('should data ok with missing file', function() {
 70 |       assert.equal(file.data('non-exist'), null);
 71 |     });
 72 |   }); // #dirAndFiles
 73 | 
 74 |   describe('#meta', function() {
 75 |     it('should meta ok within file content', function() {
 76 |       file.data = x => [
 77 |         '/ *',
 78 |         '  * @lc app=leetcode id=123 lang=javascript',
 79 |         '  * /'
 80 |       ].join('\n');
 81 |       const meta = file.meta('dummy');
 82 |       assert.equal(meta.app, 'leetcode')
 83 |       assert.equal(meta.id, '123');
 84 |       assert.equal(meta.lang, 'javascript');
 85 |     });
 86 | 
 87 |     it('should meta ok with white space', function() {
 88 |       file.data = x => [
 89 |         '/ *',
 90 |         '  * @lc app=leetcode id=123\t  \t  lang=javascript\r',
 91 |         '  * /'
 92 |       ].join('\n');
 93 |       const meta = file.meta('dummy');
 94 |       assert.equal(meta.app, 'leetcode')
 95 |       assert.equal(meta.id, '123');
 96 |       assert.equal(meta.lang, 'javascript');
 97 |     });
 98 | 
 99 |     it('should meta ok within file name', function() {
100 |       file.data = x => [
101 |         '/ *',
102 |         '  * no meta app=leetcode id=123 lang=javascript',
103 |         '  * /'
104 |       ].join('\n');
105 |       const meta = file.meta('321.dummy.py');
106 |       assert(!meta.app)
107 |       assert.equal(meta.id, '321');
108 |       assert.equal(meta.lang, 'python');
109 |     });
110 | 
111 |     it('should meta ok within deprecated file name', function() {
112 |       file.data = x => [
113 |         '/ *',
114 |         '  * no meta app=leetcode id=123 lang=javascript',
115 |         '  * /'
116 |       ].join('\n');
117 | 
118 |       var meta = file.meta('111.dummy.py3');
119 |       assert(!meta.app)
120 |       assert.equal(meta.id, '111');
121 |       assert.equal(meta.lang, 'python3');
122 | 
123 |       meta = file.meta('222.dummy.python3.py');
124 |       assert(!meta.app)
125 |       assert.equal(meta.id, '222');
126 |       assert.equal(meta.lang, 'python3');
127 |     });
128 | 
129 |     it('should fmt ok', function() {
130 |       file.init();
131 |       const data = file.fmt('${id}', {id: 123});
132 |       assert.equal(data, '123');
133 |     });
134 |   }); // #meta
135 | 
136 |   describe('#genneral', function() {
137 |     beforeEach(function() {
138 |       th.clean();
139 |     });
140 |     afterEach(function() {
141 |       th.clean();
142 |     });
143 | 
144 |     it('should mkdir ok', function() {
145 |       const dir = th.DIR + 'dir';
146 |       assert.equal(fs.existsSync(dir), false);
147 |       file.mkdir(dir);
148 |       assert.equal(fs.existsSync(dir), true);
149 |       file.mkdir(dir);
150 |       assert.equal(fs.existsSync(dir), true);
151 |     });
152 | 
153 |     it('should mv ok', function() {
154 |       const SRC = th.Dir + 'src';
155 |       const DST = th.DIR + 'dst';
156 |       assert.equal(fs.existsSync(SRC), false);
157 |       assert.equal(fs.existsSync(DST), false);
158 |       file.mkdir(SRC);
159 |       assert.equal(fs.existsSync(SRC), true);
160 |       assert.equal(fs.existsSync(DST), false);
161 |       file.mv(SRC, DST);
162 |       assert.equal(fs.existsSync(SRC), false);
163 |       assert.equal(fs.existsSync(DST), true);
164 |     });
165 |   }); // #general
166 | });
167 | 


--------------------------------------------------------------------------------
/test/test_helper.js:
--------------------------------------------------------------------------------
  1 | 'use strict';
  2 | const assert = require('chai').assert;
  3 | const rewire = require('rewire');
  4 | const _ = require('underscore');
  5 | 
  6 | const chalk = require('../lib/chalk');
  7 | 
  8 | describe('helper', function() {
  9 |   let h;
 10 | 
 11 |   before(function() {
 12 |     chalk.init();
 13 |   });
 14 | 
 15 |   beforeEach(function() {
 16 |     h = rewire('../lib/helper');
 17 |   });
 18 | 
 19 |   describe('#prettyState', function() {
 20 |     it('should ok w/ color', function() {
 21 |       chalk.enabled = true;
 22 | 
 23 |       assert.equal(h.prettyState('ac'), chalk.green('✔'));
 24 |       assert.equal(h.prettyState('notac'), chalk.red('✘'));
 25 |       assert.equal(h.prettyState('none'), ' ');
 26 |       assert.equal(h.prettyState(''), ' ');
 27 |       assert.equal(h.prettyState(null), ' ');
 28 |     });
 29 | 
 30 |     it('should ok w/o color', function() {
 31 |       chalk.enabled = false;
 32 | 
 33 |       assert.equal(h.prettyState('ac'), '✔');
 34 |       assert.equal(h.prettyState('notac'), '✘');
 35 |       assert.equal(h.prettyState('none'), ' ');
 36 |       assert.equal(h.prettyState(''), ' ');
 37 |       assert.equal(h.prettyState(null), ' ');
 38 |     });
 39 |   }); // #prettyState
 40 | 
 41 |   describe('#prettyText', function() {
 42 |     it('should ok w/ color', function() {
 43 |       chalk.enabled = true;
 44 | 
 45 |       assert.equal(h.prettyText(' text', true), chalk.green('✔ text'));
 46 |       assert.equal(h.prettyText(' text', false), chalk.red('✘ text'));
 47 |       assert.equal(h.prettyText('text'), 'text');
 48 |     });
 49 | 
 50 |     it('should ok w/o color', function() {
 51 |       chalk.enabled = false;
 52 | 
 53 |       assert.equal(h.prettyText(' text', true), '✔ text');
 54 |       assert.equal(h.prettyText(' text', false), '✘ text');
 55 |       assert.equal(h.prettyText('text'), 'text');
 56 |     });
 57 |   }); // #prettyText
 58 | 
 59 |   describe('#prettyLevel', function() {
 60 |     it('should ok w/ color', function() {
 61 |       chalk.enabled = true;
 62 | 
 63 |       assert.equal(h.prettyLevel('Easy'), chalk.green('Easy'));
 64 |       assert.equal(h.prettyLevel('Medium'), chalk.yellow('Medium'));
 65 |       assert.equal(h.prettyLevel('Hard'), chalk.red('Hard'));
 66 |       assert.equal(h.prettyLevel('easy  '), chalk.green('easy  '));
 67 |       assert.equal(h.prettyLevel('medium'), chalk.yellow('medium'));
 68 |       assert.equal(h.prettyLevel('hard  '), chalk.red('hard  '));
 69 |       assert.equal(h.prettyLevel('unknown'), 'unknown');
 70 |     });
 71 |   }); // #prettyLevel
 72 | 
 73 |   describe('#prettySize', function() {
 74 |     it('should ok', function() {
 75 |       assert.equal(h.prettySize(0), '0.00B');
 76 |       assert.equal(h.prettySize(512), '512.00B');
 77 |       assert.equal(h.prettySize(1024), '1.00K');
 78 |       assert.equal(h.prettySize(1024 * 1024), '1.00M');
 79 |       assert.equal(h.prettySize(1024 * 1024 * 1024), '1.00G');
 80 |     });
 81 |   }); // #prettySize
 82 | 
 83 |   describe('#prettyTime', function() {
 84 |     it('should ok', function() {
 85 |       assert.equal(h.prettyTime(30), '30 seconds');
 86 |       assert.equal(h.prettyTime(60), '1 minutes');
 87 |       assert.equal(h.prettyTime(2400), '40 minutes');
 88 |       assert.equal(h.prettyTime(3600), '1 hours');
 89 |       assert.equal(h.prettyTime(7200), '2 hours');
 90 |       assert.equal(h.prettyTime(86400), '1 days');
 91 |       assert.equal(h.prettyTime(86400 * 3), '3 days');
 92 |       assert.equal(h.prettyTime(86400 * 7), '1 weeks');
 93 |     });
 94 |   }); // #prettyTime
 95 | 
 96 |   describe('#levelToName', function() {
 97 |     it('should ok', function() {
 98 |       assert.equal(h.levelToName(0), ' ');
 99 |       assert.equal(h.levelToName(1), 'Easy');
100 |       assert.equal(h.levelToName(2), 'Medium');
101 |       assert.equal(h.levelToName(3), 'Hard');
102 |       assert.equal(h.levelToName(4), ' ');
103 |     });
104 |   }); // #levelToName
105 | 
106 |   describe('#statusToName', function() {
107 |     it('should ok', function() {
108 |       assert.equal(h.statusToName(10), 'Accepted');
109 |       assert.equal(h.statusToName(11), 'Wrong Answer');
110 |       assert.equal(h.statusToName(12), 'Memory Limit Exceeded');
111 |       assert.equal(h.statusToName(13), 'Output Limit Exceeded');
112 |       assert.equal(h.statusToName(14), 'Time Limit Exceeded');
113 |       assert.equal(h.statusToName(15), 'Runtime Error');
114 |       assert.equal(h.statusToName(16), 'Internal Error');
115 |       assert.equal(h.statusToName(20), 'Compile Error');
116 |       assert.equal(h.statusToName(21), 'Unknown Error');
117 |       assert.equal(h.statusToName(99), 'Unknown');
118 |     });
119 |   }); // #statusToName
120 | 
121 |   describe('#langToExt', function() {
122 |     it('should ok', function() {
123 |       assert.equal(h.langToExt('bash'), '.sh');
124 |       assert.equal(h.langToExt('c'), '.c');
125 |       assert.equal(h.langToExt('cpp'), '.cpp');
126 |       assert.equal(h.langToExt('csharp'), '.cs');
127 |       assert.equal(h.langToExt('golang'), '.go');
128 |       assert.equal(h.langToExt('java'), '.java');
129 |       assert.equal(h.langToExt('javascript'), '.js');
130 |       assert.equal(h.langToExt('mysql'), '.sql');
131 |       assert.equal(h.langToExt('php'), '.php');
132 |       assert.equal(h.langToExt('python'), '.py');
133 |       assert.equal(h.langToExt('python3'), '.py');
134 |       assert.equal(h.langToExt('ruby'), '.rb');
135 |       assert.equal(h.langToExt('rust'), '.rs');
136 |       assert.equal(h.langToExt('scala'), '.scala');
137 |       assert.equal(h.langToExt('swift'), '.swift');
138 |       assert.equal(h.langToExt('rust'), '.rs');
139 |       assert.equal(h.langToExt('typescript'), '.ts');
140 |     });
141 |   }); // #langToExt
142 | 
143 |   describe('#extToLang', function() {
144 |     it('should ok', function() {
145 |       assert.equal(h.extToLang('/usr/bin/file.sh'), 'bash');
146 |       assert.equal(h.extToLang('/home/skygragon/file.c'), 'c');
147 |       assert.equal(h.extToLang('/var/log/file.cpp'), 'cpp');
148 |       assert.equal(h.extToLang('./file.cs'), 'csharp');
149 |       assert.equal(h.extToLang('../file.go'), 'golang');
150 |       assert.equal(h.extToLang('file.java'), 'java');
151 |       assert.equal(h.extToLang('~/leetcode/../file.sql'), 'mysql');
152 |       assert.equal(h.extToLang('~/leetcode/hello.php'), 'php');
153 |       assert.equal(h.extToLang('c:/file.js'), 'javascript');
154 |       assert.equal(h.extToLang('c:/Users/skygragon/file.py'), 'python');
155 |       assert.equal(h.extToLang('~/file.rb'), 'ruby');
156 |       assert.equal(h.extToLang('~/leetcode/file.rs'), 'rust');
157 |       assert.equal(h.extToLang('/tmp/file.scala'), 'scala');
158 |       assert.equal(h.extToLang('~/leetcode/file.swift'), 'swift');
159 |       assert.equal(h.extToLang('~/leetcode/../file.sql'), 'mysql');
160 |       assert.equal(h.extToLang('/home/skygragon/file.dat'), 'unknown');
161 |       assert.equal(h.extToLang('~/leetcode/file.rs'), 'rust');
162 |       assert.equal(h.extToLang('~/leetcode/file.ts'), 'typescript');
163 |     });
164 |   }); // #extToLang
165 | 
166 |   describe('#langToCommentStyle', function() {
167 |     it('should ok', function() {
168 |       const C_STYLE = {start: '/*', line: ' *', end: ' */', singleLine: '//'};
169 |       const RUBY_STYLE = {start: '#', line: '#', end: '#', singleLine: '#'};
170 |       const SQL_STYLE = {start: '--', line: '--', end: '--', singleLine: '--'};
171 | 
172 |       assert.deepEqual(h.langToCommentStyle('bash'), RUBY_STYLE);
173 |       assert.deepEqual(h.langToCommentStyle('c'), C_STYLE);
174 |       assert.deepEqual(h.langToCommentStyle('cpp'), C_STYLE);
175 |       assert.deepEqual(h.langToCommentStyle('csharp'), C_STYLE);
176 |       assert.deepEqual(h.langToCommentStyle('golang'), C_STYLE);
177 |       assert.deepEqual(h.langToCommentStyle('php'), C_STYLE);
178 |       assert.deepEqual(h.langToCommentStyle('java'), C_STYLE);
179 |       assert.deepEqual(h.langToCommentStyle('javascript'), C_STYLE);
180 |       assert.deepEqual(h.langToCommentStyle('mysql'), SQL_STYLE);
181 |       assert.deepEqual(h.langToCommentStyle('rust'), C_STYLE);
182 |       assert.deepEqual(h.langToCommentStyle('python'), RUBY_STYLE);
183 |       assert.deepEqual(h.langToCommentStyle('python3'), RUBY_STYLE);
184 |       assert.deepEqual(h.langToCommentStyle('ruby'), RUBY_STYLE);
185 |       assert.deepEqual(h.langToCommentStyle('scala'), C_STYLE);
186 |       assert.deepEqual(h.langToCommentStyle('swift'), C_STYLE);
187 |       assert.deepEqual(h.langToCommentStyle('typescript'), C_STYLE);
188 |     });
189 |   }); // #langToCommentStyle
190 | 
191 |   describe('#getSetCookieValue', function() {
192 |     it('should ok', function() {
193 |       const resp = {
194 |         headers: {'set-cookie': [
195 |           'key1=value1; path=/; Httponly',
196 |           'key2=value2; path=/; Httponly']
197 |         }
198 |       };
199 |       const respNoSetCookie = {
200 |         headers: {}
201 |       };
202 | 
203 |       assert.equal(h.getSetCookieValue(resp, 'key1'), 'value1');
204 |       assert.equal(h.getSetCookieValue(resp, 'key2'), 'value2');
205 |       assert.equal(h.getSetCookieValue(resp, 'key3'), null);
206 |       assert.equal(h.getSetCookieValue(respNoSetCookie, 'key1'), null);
207 |     });
208 |   }); // #getSetCookieValue
209 | 
210 |   describe('#printSafeHTTP', function() {
211 |     it('should hide sensitive info', function() {
212 |       const raw = [
213 |         "Cookie: 'xxxxxx'",
214 |         "'X-CSRFToken': 'yyyyyy'",
215 |         "'set-cookie': ['zzzzzz']"
216 |       ].join('\r\n');
217 | 
218 |       const hide = [
219 |         'Cookie: ',
220 |         "'X-CSRFToken': ",
221 |         "'set-cookie': "
222 |       ].join('\r\n');
223 | 
224 |       assert.equal(h.printSafeHTTP(raw), hide);
225 |     });
226 |   }); // #printSafeHTTP
227 | 
228 |   describe('#readStdin', function() {
229 |     function hijackStdin(data) {
230 |       const stream = require('stream');
231 |       const rs = new stream.Readable();
232 |       rs.push(data);
233 |       rs.push(null);
234 | 
235 |       Object.defineProperty(process, 'stdin', {value: rs});
236 |     }
237 | 
238 |     it('should ok', function(done) {
239 |       hijackStdin('[1,2]\n3');
240 | 
241 |       h.readStdin(function(e, data) {
242 |         assert.equal(data, '[1,2]\n3');
243 |         done();
244 |       });
245 |     });
246 | 
247 |     it('should ok w/ empty input', function(done) {
248 |       hijackStdin('');
249 | 
250 |       h.readStdin(function(e, data) {
251 |         assert.equal(data, '');
252 |         done();
253 |       });
254 |     });
255 |   }); // #readStdin
256 | 
257 |   describe('#badge', function() {
258 |     it('should ok', function() {
259 |       chalk.enabled = true;
260 |       assert.equal(h.badge('x'), chalk.white.bgBlue(' x '));
261 |       assert.equal(h.badge('x', 'green'), chalk.black.bgGreen(' x '));
262 |     });
263 | 
264 |     it('should ok with random', function() {
265 |       const badges = _.values(h.__get__('COLORS'))
266 |         .map(function(x) {
267 |           return chalk[x.fg][x.bg](' random ');
268 |         });
269 | 
270 |       const i = badges.indexOf(h.badge('random', 'random'));
271 |       assert.equal(i >= 0, true);
272 |     });
273 |   }); // #badge
274 | });
275 | 


--------------------------------------------------------------------------------
/test/test_icon.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | const assert = require('chai').assert;
 3 | const rewire = require('rewire');
 4 | 
 5 | describe('icon', function() {
 6 |   let icon;
 7 |   let file;
 8 | 
 9 |   beforeEach(function() {
10 |     file = rewire('../lib/file');
11 |     file.listCodeDir = function() {
12 |       return [
13 |         {name: 'mac', data: {yes: 'yes', no: 'no', lock: 'lock', like: 'like', unlike: 'unlike'}},
14 |         {name: 'win7', data: {yes: 'YES', no: 'NO', lock: 'LOCK', like: 'LIKE', unlike: 'UNLIKE'}}
15 |       ];
16 |     };
17 | 
18 |     icon = rewire('../lib/icon');
19 |     icon.__set__('file', file);
20 |     icon.init();
21 |   });
22 | 
23 |   describe('#setTheme', function() {
24 |     it('should ok with known theme', function() {
25 |       icon.setTheme('mac');
26 |       assert.equal(icon.yes, 'yes');
27 |       assert.equal(icon.no, 'no');
28 |       assert.equal(icon.lock, 'lock');
29 |       assert.equal(icon.like, 'like');
30 |       assert.equal(icon.unlike, 'unlike');
31 |     });
32 | 
33 |     it('should ok with unknown theme on linux', function() {
34 |       file.isWindows = () => false;
35 | 
36 |       icon.setTheme('non-exist');
37 |       assert.equal(icon.yes, '✔');
38 |       assert.equal(icon.no, '✘');
39 |       assert.equal(icon.lock, '🔒');
40 |       assert.equal(icon.like, '★');
41 |       assert.equal(icon.unlike, '☆');
42 |     });
43 | 
44 |     it('should ok with unknown theme on windows', function() {
45 |       file.isWindows = () => true;
46 | 
47 |       icon.setTheme('non-exist');
48 |       assert.equal(icon.yes, 'YES');
49 |       assert.equal(icon.no, 'NO');
50 |       assert.equal(icon.lock, 'LOCK');
51 |       assert.equal(icon.like, 'LIKE');
52 |       assert.equal(icon.unlike, 'UNLIKE');
53 |     });
54 |   }); // #setTheme
55 | });
56 | 


--------------------------------------------------------------------------------
/test/test_log.js:
--------------------------------------------------------------------------------
  1 | 'use strict';
  2 | const assert = require('chai').assert;
  3 | const rewire = require('rewire');
  4 | 
  5 | const chalk = require('../lib/chalk');
  6 | 
  7 | describe('log', function() {
  8 |   let log;
  9 |   let savedOutput;
 10 |   let expected;
 11 | 
 12 |   before(function() {
 13 |     chalk.init();
 14 |   });
 15 | 
 16 |   beforeEach(function() {
 17 |     log = rewire('../lib/log');
 18 |     savedOutput = log.output;
 19 |     log.output = x => expected = x;
 20 | 
 21 |     log.init();
 22 |     expected = '';
 23 |   });
 24 | 
 25 |   afterEach(function() {
 26 |     log.output = savedOutput;
 27 |   });
 28 | 
 29 |   describe('#setLevel', function() {
 30 |     it('should ok with known level', function() {
 31 |       log.setLevel('TRACE');
 32 |       assert.deepEqual(log.level, log.levels.get('TRACE'));
 33 |       log.setLevel('DEBUG');
 34 |       assert.deepEqual(log.level, log.levels.get('DEBUG'));
 35 |       log.setLevel('INFO');
 36 |       assert.deepEqual(log.level, log.levels.get('INFO'));
 37 |       log.setLevel('WARN');
 38 |       assert.deepEqual(log.level, log.levels.get('WARN'));
 39 |       log.setLevel('ERROR');
 40 |       assert.deepEqual(log.level, log.levels.get('ERROR'));
 41 |     });
 42 | 
 43 |     it('should ok with unknown level', function() {
 44 |       log.setLevel('');
 45 |       assert.deepEqual(log.level, log.levels.get('INFO'));
 46 |     });
 47 |   }); // #setLevel
 48 | 
 49 |   describe('#isEnabled', function() {
 50 |     it('should ok', function() {
 51 |       log.setLevel('DEBUG');
 52 |       assert.equal(log.isEnabled('TRACE'), false);
 53 |       assert.equal(log.isEnabled('DEBUG'), true);
 54 |       assert.equal(log.isEnabled('INFO'), true);
 55 |       assert.equal(log.isEnabled('WARN'), true);
 56 |       assert.equal(log.isEnabled('ERROR'), true);
 57 |     });
 58 |   }); // #isEnabled
 59 | 
 60 |   describe('#levels', function() {
 61 |     it('should ok with log.trace', function() {
 62 |       log.trace('some error');
 63 |       assert.equal(expected, '');
 64 | 
 65 |       log.setLevel('TRACE');
 66 |       log.trace('some error');
 67 |       assert.equal(expected, chalk.gray('[TRACE] some error'));
 68 |     });
 69 | 
 70 |     it('should ok with log.debug', function() {
 71 |       log.debug('some error');
 72 |       assert.equal(expected, '');
 73 | 
 74 |       log.setLevel('DEBUG');
 75 |       log.debug('some error');
 76 |       assert.equal(expected, chalk.gray('[DEBUG] some error'));
 77 |     });
 78 | 
 79 |     it('should ok with log.info', function() {
 80 |       log.info('some error');
 81 |       assert.equal(expected, 'some error');
 82 |     });
 83 | 
 84 |     it('should ok with log.warn', function() {
 85 |       log.warn('some error');
 86 |       assert.equal(expected, chalk.yellow('[WARN] some error'));
 87 |     });
 88 | 
 89 |     it('should ok with log.error', function() {
 90 |       log.error('some error');
 91 |       assert.equal(expected, chalk.red('[ERROR] some error'));
 92 |     });
 93 | 
 94 |     it('should ok with log.fail', function() {
 95 |       log.fail({msg: 'some error', statusCode: 500});
 96 |       assert.equal(expected, chalk.red('[ERROR] some error [code=500]'));
 97 | 
 98 |       log.fail('some error');
 99 |       assert.equal(expected, chalk.red('[ERROR] some error'));
100 |     });
101 |   }); // #levels
102 | 
103 |   describe('#printf', function() {
104 |     it('should ok', function() {
105 |       log.printf('%s and %s and %%', 'string', 100);
106 |       assert.equal(expected, 'string and 100 and %');
107 |     });
108 |   }); // #printf
109 | });
110 | 


--------------------------------------------------------------------------------
/test/test_plugin.js:
--------------------------------------------------------------------------------
  1 | 'use strict';
  2 | const fs = require('fs');
  3 | const path = require('path');
  4 | 
  5 | const assert = require('chai').assert;
  6 | const rewire = require('rewire');
  7 | 
  8 | const chalk = require('../lib/chalk');
  9 | const config = require('../lib/config');
 10 | const log = require('../lib/log');
 11 | const th = require('./helper');
 12 | 
 13 | const Plugin = rewire('../lib/plugin');
 14 | 
 15 | describe('plugin', function() {
 16 |   let file;
 17 |   let cache;
 18 | 
 19 |   const NOOP = () => {};
 20 | 
 21 |   before(function() {
 22 |     log.init();
 23 |     chalk.init();
 24 |     config.init();
 25 | 
 26 |     file = rewire('../lib/file');
 27 |     cache = rewire('../lib/cache');
 28 |     Plugin.__set__('file', file);
 29 |     Plugin.__set__('cache', cache);
 30 |   });
 31 | 
 32 |   beforeEach(function() {
 33 |     th.clean();
 34 |     cache.get = NOOP;
 35 |   });
 36 | 
 37 |   describe('#Plugin.init', function() {
 38 |     const p1 = new Plugin(0, 'leetcode', '2.0');
 39 |     const p2 = new Plugin(1, 'cache', '1.0');
 40 |     const p3 = new Plugin(2, 'retry', '3.0');
 41 |     const p4 = new Plugin(3, 'core', '4.0');
 42 | 
 43 |     before(function() {
 44 |       p1.init = p2.init = p3.init = p4.init = NOOP;
 45 |       file.listCodeDir = function() {
 46 |         return [
 47 |           {name: 'cache', data: p2, file: 'cache.js'},
 48 |           {name: 'leetcode', data: p1, file: 'leetcode.js'},
 49 |           {name: 'retry', data: p3, file: 'retry.js'},
 50 |           {name: 'bad', data: null}
 51 |         ];
 52 |       };
 53 |     });
 54 | 
 55 |     it('should init ok', function() {
 56 |       cache.get = () => {
 57 |         return {cache: true, leetcode: false, retry: true};
 58 |       };
 59 |       assert.deepEqual(Plugin.plugins, []);
 60 | 
 61 |       const res = Plugin.init(p4);
 62 |       assert.equal(res, true);
 63 |       assert.deepEqual(Plugin.plugins.length, 3);
 64 | 
 65 |       const names = Plugin.plugins.map(p => p.name);
 66 |       assert.deepEqual(names, ['retry', 'cache', 'leetcode']);
 67 | 
 68 |       assert.equal(p4.next, p3);
 69 |       assert.equal(p3.next, p2);
 70 |       assert.equal(p2.next, null);
 71 |       assert.equal(p1.next, null);
 72 |     });
 73 | 
 74 |     it('should find missing ok', function() {
 75 |       cache.get = () => {
 76 |         return {company: true, leetcode: false, solution: true};
 77 |       };
 78 | 
 79 |       const res = Plugin.init(p4);
 80 |       assert.equal(res, false);
 81 |       assert.deepEqual(Plugin.plugins.length, 5);
 82 | 
 83 |       const names = Plugin.plugins.map(p => p.name);
 84 |       assert.deepEqual(names, ['retry', 'cache', 'leetcode', 'company', 'solution']);
 85 | 
 86 |       assert.equal(p4.next, p3);
 87 |       assert.equal(p3.next, p2);
 88 |       assert.equal(p2.next, null);
 89 |       assert.equal(p1.next, null);
 90 |     });
 91 |   }); // #Plugin.init
 92 | 
 93 |   describe('#install', function() {
 94 |     let expected;
 95 | 
 96 |     before(function() {
 97 |       Plugin.__set__('cp', {
 98 |         exec: function(cmd, opts, cb) {
 99 |           expected = cmd;
100 |           return cb();
101 |         }
102 |       });
103 |     });
104 | 
105 |     it('should install no deps ok', function(done) {
106 |       expected = '';
107 |       const p = new Plugin(100, 'test', '2017.12.26', 'desc', []);
108 |       p.install(function() {
109 |         assert.equal(expected, '');
110 |         done();
111 |       });
112 |     });
113 | 
114 |     it('should install deps ok', function(done) {
115 |       const deps = ['a', 'b:linux', 'b:darwin', 'b:win32', 'c:bad', 'd'];
116 |       const p = new Plugin(100, 'test', '2017.12.26', 'desc', deps);
117 |       p.install(function() {
118 |         assert.equal(expected, 'npm install --save a b d');
119 |         done();
120 |       });
121 |     });
122 |   }); // #install
123 | 
124 |   describe('#Plugin.copy', function() {
125 |     const SRC = path.resolve(th.DIR, 'copy.src.js');
126 |     const DST = path.resolve(th.DIR, 'copy.test.js');
127 | 
128 |     before(function() {
129 |       file.pluginFile = () => DST;
130 |     });
131 | 
132 |     it('should copy from http error', function(done) {
133 |       Plugin.copy('non-exists', function(e, fullpath) {
134 |         assert.equal(e, 'HTTP Error: 404');
135 |         assert.equal(fs.existsSync(DST), false);
136 |         done();
137 |       });
138 |     }).timeout(5000);
139 | 
140 |     it('should copy from local ok', function(done) {
141 |       const data = [
142 |         'module.exports = {',
143 |         '  x: 123,',
144 |         '  install: function(cb) { cb(); }',
145 |         '};'
146 |       ];
147 |       fs.writeFileSync(SRC, data.join('\n'));
148 | 
149 |       Plugin.copy(SRC, function(e, fullpath) {
150 |         assert.notExists(e);
151 |         assert.equal(fullpath, DST);
152 |         assert.equal(fs.existsSync(DST), true);
153 |         done();
154 |       });
155 |     });
156 |   }); // #Plugin.copy
157 | 
158 |   describe('#Plugin.installMissings', function() {
159 |     const PLUGINS = [
160 |       new Plugin(0, '0', 'missing'),
161 |       new Plugin(1, '1', '2018.01.01'),
162 |       new Plugin(2, '2', 'missing'),
163 |     ];
164 |     let expected;
165 | 
166 |     beforeEach(function() {
167 |       expected = [];
168 |       file.pluginFile = x => th.DIR + x;
169 |       Plugin.install = (name, cb) => {
170 |         expected.push(name);
171 |         return cb(null, PLUGINS[+name]);
172 |       };
173 |     });
174 | 
175 |     it('should ok', function(done) {
176 |       Plugin.plugins = PLUGINS;
177 |       Plugin.installMissings(function(e) {
178 |         assert.notExists(e);
179 |         assert.deepEqual(expected, ['0', '2']);
180 |         done();
181 |       });
182 |     });
183 |   }); // #Plugin.installMissings
184 | 
185 |   describe('#delete', function() {
186 |     it('should ok', function() {
187 |       file.pluginFile = x => th.DIR + x;
188 | 
189 |       const p = new Plugin(0, '0', '2018.01.01');
190 |       p.file = '0.js';
191 |       fs.writeFileSync('./tmp/0.js', '');
192 | 
193 |       assert.equal(p.deleted, false);
194 |       assert.deepEqual(fs.readdirSync(th.DIR), ['0.js']);
195 |       p.delete();
196 |       assert.equal(p.deleted, true);
197 |       assert.deepEqual(fs.readdirSync(th.DIR), []);
198 |       p.delete();
199 |       assert.equal(p.deleted, true);
200 |       assert.deepEqual(fs.readdirSync(th.DIR), []);
201 |     });
202 |   }); // #delete
203 | 
204 |   describe('#save', function() {
205 |     it('should ok', function() {
206 |       let data = {};
207 |       cache.get = () => data;
208 |       cache.set = (k, x) => data = x;
209 | 
210 |       const p = new Plugin(0, '0', '2018.01.01');
211 |       p.save();
212 |       assert.deepEqual(data, {'0': true});
213 | 
214 |       p.enabled = false;
215 |       p.save();
216 |       assert.deepEqual(data, {'0': false});
217 | 
218 |       p.deleted = true;
219 |       p.save();
220 |       assert.deepEqual(data, {});
221 |     });
222 |   }); // #save
223 | });
224 | 


--------------------------------------------------------------------------------
/test/test_queue.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | const assert = require('chai').assert;
 3 | const rewire = require('rewire');
 4 | 
 5 | 
 6 | 
 7 | describe('queue', function() {
 8 |   let Queue;
 9 | 
10 |   beforeEach(function() {
11 |     Queue = rewire('../lib/queue');
12 |   });
13 | 
14 |   it('should ok', function(done) {
15 |     function doTask(x, q, cb) {
16 |       ++q.ctx.n;
17 |       q.ctx.sum += x;
18 |       return cb();
19 |     }
20 | 
21 |     const ctx = {n: 0, sum: 0};
22 |     const q = new Queue([], ctx, doTask);
23 | 
24 |     q.addTask(1);
25 |     q.addTask(2);
26 |     q.addTasks([3, 4, 5]);
27 | 
28 |     q.run(5, function(e, ctx) {
29 |       assert.notExists(e);
30 |       assert.equal(ctx.n, 5);
31 |       assert.equal(ctx.sum, 15);
32 |       done();
33 |     });
34 |   });
35 | 
36 |   it('should ok in sequence', function(done) {
37 |     const config = {network: {}};
38 |     Queue.__set__('config', config);
39 | 
40 |     function doTask(x, q, cb) {
41 |       if (!q.ctx.list) q.ctx.list = [];
42 |       q.ctx.list.push(x);
43 |       return cb();
44 |     }
45 | 
46 |     const q = new Queue(null, null, doTask);
47 |     q.addTask(1);
48 |     q.addTasks([2, 3]);
49 |     q.addTasks([4]);
50 |     q.addTask(5);
51 | 
52 |     q.run(null, function(e, ctx) {
53 |       assert.notExists(e);
54 |       assert.deepEqual(ctx.list, [1, 2, 3, 4, 5]);
55 |       done();
56 |     });
57 |   });
58 | });
59 | 


--------------------------------------------------------------------------------
/test/test_session.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | const assert = require('chai').assert;
 3 | const rewire = require('rewire');
 4 | 
 5 | describe('session', function() {
 6 |   let session;
 7 |   let stats;
 8 |   let now;
 9 | 
10 |   beforeEach(function() {
11 |     stats = null;
12 |     const cache = {
13 |       get: (k) => stats,
14 |       set: (k, v) => stats = v
15 |     };
16 |     const moment = () => {
17 |       return {format: () => now}
18 |     };
19 | 
20 |     session = rewire('../lib/session');
21 |     session.__set__('cache', cache);
22 |     session.__set__('moment', moment);
23 |   });
24 | 
25 |   describe('#updateStat', function() {
26 |     it('should update number ok', function() {
27 |       now = '2017.12.13';
28 |       session.updateStat('ac', 10);
29 |       assert.deepEqual(stats, {'2017.12.13': {ac: 10}});
30 | 
31 |       session.updateStat('ac', 20);
32 |       assert.deepEqual(stats, {'2017.12.13': {ac: 30}});
33 | 
34 |       now = '2017.12.14';
35 |       session.updateStat('ac', 40);
36 |       assert.deepEqual(stats, {
37 |         '2017.12.13': {ac: 30},
38 |         '2017.12.14': {ac: 40}
39 |       });
40 |     });
41 | 
42 |     it('should update set ok', function() {
43 |       now = '2017.12.13';
44 |       session.updateStat('ac.set', 101);
45 |       assert.deepEqual(stats, {'2017.12.13': {'ac.set': [101]}});
46 |       session.updateStat('ac.set', 100);
47 |       assert.deepEqual(stats, {'2017.12.13': {'ac.set': [101, 100]}});
48 |       session.updateStat('ac.set', 101);
49 |       assert.deepEqual(stats, {'2017.12.13': {'ac.set': [101, 100]}});
50 |     });
51 |   }); // #updateStat
52 | });
53 | 


--------------------------------------------------------------------------------
/test/test_sprintf.js:
--------------------------------------------------------------------------------
 1 | 'use strict';
 2 | const assert = require('chai').assert;
 3 | const rewire = require('rewire');
 4 | 
 5 | const sprintf = require('../lib/sprintf');
 6 | 
 7 | describe('sprintf', function() {
 8 |   it('should ok', function() {
 9 |     assert.equal(sprintf('%%'), '%');
10 |     assert.equal(sprintf('%s', 123), '123');
11 |     assert.equal(sprintf('%6s', 123), '   123');
12 |     assert.equal(sprintf('%06s', 123), '000123');
13 |     assert.equal(sprintf('%-6s', 123), '123   ');
14 |     assert.equal(sprintf('%=6s', 123), '  123 ');
15 | 
16 |     assert.equal(sprintf('%4s,%=4s,%-4s', 123, 'xy', 3.1), ' 123, xy ,3.1 ');
17 |   });
18 | 
19 |   it('should non-ascii ok', function() {
20 |     assert.equal(sprintf('%4s', '中'), '  中');
21 |     assert.equal(sprintf('%-4s', '中'), '中  ');
22 |     assert.equal(sprintf('%=4s', '中'), ' 中 ');
23 | 
24 |     assert.equal(sprintf('%=14s', '12你好34世界'), ' 12你好34世界 ');
25 |   });
26 | 
27 |   it('should color ok', function() {
28 |     const chalk = rewire('../lib/chalk');
29 |     chalk.init();
30 | 
31 |     assert.equal(sprintf('%=3s', chalk.red('X')), ' ' + chalk.red('X') + ' ');
32 |   });
33 | });
34 | 


--------------------------------------------------------------------------------