diff --git a/.dockerignore b/.dockerignore
index ed775dc7..c34cc8cd 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,10 +1,14 @@
 .git
+.github
 .npm
 .nyc_output
 .DS_Store
+
 coverage
+dist
 node_modules
 npm-debug.log*
 tmp
+
 *.log
 *.swp
diff --git a/.eslintrc.js b/.eslintrc.js
index 8cce4ef7..5ef04108 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -17,6 +17,7 @@ module.exports = {
       "curly": 0,
       "key-spacing": [2, {align: "value"}],
       "max-len": [1, 120],
+      "no-control-regex": 0,
       "no-console": 1,
       "no-empty": [2, { "allowEmptyCatch": true }],
       "no-eval": 1, // we use it on purpose
diff --git a/.gitignore b/.gitignore
index a833ee3d..c08d1d39 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,6 +36,7 @@ jspm_packages
 # Optional REPL history
 .node_repl_history
 
+dist/
 tmp/
 *.swp
 .DS_Store
diff --git a/.travis.yml b/.travis.yml
index e9f9d47a..247f9281 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,26 @@
 language: node_js
 node_js:
-  - stable
+  - 10
+
+os:
+  - linux
+  - osx
+  - windows
+
 install:
   - npm install
+
+script:
+  - npm test
+  - if [ -n "$TRAVIS_TAG" ]; then npm run travis; fi
+
+deploy:
+  provider: releases
+  api_key:
+    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="
+  file: leetcode-cli.*
+  file_glob: true
+  skip_cleanup: true
+  overwrite: true
+  on:
+    tags: true
diff --git a/bin/pkg b/bin/pkg
new file mode 100755
index 00000000..0cc22db1
--- /dev/null
+++ b/bin/pkg
@@ -0,0 +1,20 @@
+#!/usr/bin/env node
+
+const arch = require('os').arch();
+var os = process.platform;
+const ver = process.versions.node.split('.')[0];
+
+var bin = './bin/pkg.sh';
+var args = [arch, os, ver];
+
+if (os === 'darwin') {
+  args[1] = 'macos';
+} else if (os === 'win32') {
+  bin = 'cmd.exe';
+  args = ['/c', 'bin\\pkg.bat'].concat(args);
+}
+
+var proc = require('child_process').spawn(bin, args);
+proc.stdout.on('data', x => console.log(x.toString().trimRight('\n')));
+proc.stderr.on('data', x => console.log(x.toString().trimRight('\n')));
+proc.on('close', process.exit);
\ No newline at end of file
diff --git a/bin/pkg.bat b/bin/pkg.bat
new file mode 100644
index 00000000..0294b7f5
--- /dev/null
+++ b/bin/pkg.bat
@@ -0,0 +1,25 @@
+@echo off
+set arch=%1
+set os=%2
+set ver=%3
+
+set dist=dist\
+set file=leetcode-cli.node%ver%.%os%.%arch%.zip
+
+mkdir %dist%
+del /q %dist%\*
+del /q *.zip
+
+for %%x in (company cookie.chrome cookie.firefox cpp.lint cpp.run github leetcode.cn lintcode solution.discuss) do (
+    echo [%%x]
+    node bin\leetcode ext -i %%x
+    if %ERRORLEVEL% gtr 0 exit /b 1
+)
+
+for /r . %%x in (*.node) do copy %%x %dist%
+call npm run pkg -- node%ver%-%os%-%arch%
+if %ERRORLEVEL% gtr 0 exit /b 1
+
+7z a %file% %dist%
+if %ERRORLEVEL% gtr 0 exit /b 1
+exit 0
\ No newline at end of file
diff --git a/bin/pkg.sh b/bin/pkg.sh
new file mode 100755
index 00000000..abeb778d
--- /dev/null
+++ b/bin/pkg.sh
@@ -0,0 +1,26 @@
+#!/bin/bash -e
+
+arch=$1
+os=$2
+ver=$3
+
+DIST=./dist
+FILE=leetcode-cli.node$ver.$os.$arch.tar.gz
+
+mkdir -p $DIST
+rm -rf $DIST/*
+rm -rf $FILE
+
+plugins="company cookie.chrome cookie.firefox cpp.lint cpp.run github leetcode.cn lintcode solution.discuss"
+
+for plugin in $plugins; do
+  echo "[$plugin]"
+  ./bin/leetcode ext -i $plugin
+done
+
+find node_modules -name "*.node" -exec cp {} $DIST \;
+npm run pkg -- node$ver-$os-$arch
+
+tar zcvf $FILE $DIST
+ls -al $FILE
+exit 0
\ No newline at end of file
diff --git a/docs/advanced.md b/docs/advanced.md
index a7cc593b..42904e91 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -9,6 +9,7 @@ title: Advanced Topic
 * [Cache](#cache)
 * [Configuration](#configuration)
 * [Color Themes](#color-themes)
+* [File Name](#file-name)
 * [Log Levels](#log-levels)
 * [Plugins](#plugins)
 
@@ -91,6 +92,10 @@ The config file is saved in `~/.lc/config.json`, here is a full exmaple (include
             "enable": true,
             "theme": "default"
         },
+        "file": {
+            "show": "${fid}.${slug}",
+            "submission": "${fid}.${slug}.${sid}.${ac}"
+        },
         "icon": {
             "theme": ""
         },
@@ -107,6 +112,7 @@ Here are some useful settings:
 * `code:lang` to set your default language used in coding.
 * `color:enable` to enable colorful output.
 * `color:theme` to set color theme used in output. (see [Color Theme](#color-theme))
+* `file.show` to set filename pattern for generated code file. (see [File Name](#file-name))
 * `icon:theme` to set icon them used in output.
 * `plugins` to config each installed plugins. (see [Plugins](#plugins))
 
@@ -167,6 +173,23 @@ Of course you can create your own themes if you like, look into `colors` folder
         "yellow":  "#ffff00"
     }
 
+# File Name
+
+You could configure file name pattern in code generation.
+
+* config `file.show` for generated file in `show`.
+* config `file.submission` for downloaded file in `submission`.
+
+Followings are some variables you could used in the pattern:
+
+* `${fid}` for question id. (e.g. `123`)
+* `${slug}` for dash-separated question name. (e.g. `add-two`)
+* `${name}` for space-separated questions name. (e.g. `Add Two`)
+* `${level}` for question level. (e.g. `Hard`)
+* `${category}` for question category. (e.g. `algorithms`)
+* `${sid}` for submission id.
+* `${ac}` for accept status of existing submission.
+
 # Log Levels
 
 * `-v` to enable debug output.
diff --git a/docs/commands.md b/docs/commands.md
index 6ff28658..2eda14a7 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -465,7 +465,7 @@ Display version information.
 Short:
 
     $ leetcode version
-    2.5.2
+    2.6.1
 
 Verbose:
 
@@ -475,7 +475,7 @@ Verbose:
     | | ___  ___| |_  ___  ___   __| | ___
     | |/ _ \/ _ \ __|/ __|/ _ \ / _` |/ _ \
     | |  __/  __/ |_  (__| (_) | (_| |  __/
-    |_|\___|\___|\__|\___|\___/ \__,_|\___|  CLI v2.5.2
+    |_|\___|\___|\__|\___|\___/ \__,_|\___|  CLI v2.6.1
 
     [Environment]
     Node                 v8.1.4
diff --git a/docs/install.md b/docs/install.md
index 5b8e0706..c2e20595 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -3,6 +3,12 @@ layout: default
 title: Installation
 ---
 
+# All in One (beta)
+
+No need to install node.js. Now available on 64bits linux, mac, and windows.
+
+[Download](https://github.com/skygragon/leetcode-cli/releases)
+
 # Prerequisites
 
 Install the latest LTS version of `node.js` (`npm` included):
@@ -49,6 +55,11 @@ Similar with above, while you can introduce your own changes as you wish.
     $ cd leetcode-cli && ./bin/install
     $ leetcode version
 
+### From source (all-in-one)
+
+	$ git clone http://github.com/skygragon/leetcode-cli
+	$ cd leetcode-cli && node ./bin/pkg
+
 ### From docker
 
 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.
diff --git a/docs/releases.md b/docs/releases.md
index e2bd6a12..7b78a5f3 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -2,6 +2,32 @@
 layout: default
 title: Release Notes
 ---
+# 2.6.1
+* `submit`
+    * fixes 500 error on windows.
+
+# 2.6.0
+* build all-in-one binary for linux/macos/windows.
+* `show`
+    * support customized filename.
+    * use "--" as comment in sql file.
+* `list`
+    * fixes format issue.
+* fixes UT failures on windows.
+
+# 2.5.4
+* fixes error in fresh env without .lc existed.
+* embed meta in file content instead of file name.
+* update dependencies.
+
+# 2.5.3
+
+* fixes "Failed to load locked problem" issue.
+* move plugin's data into separate folders:
+  * login info
+  * problems list
+  * problem cache
+
 # 2.5.2
 
 * `show`
diff --git a/icons/ascii.json b/icons/ascii.json
index 6660733f..1a7664f1 100644
--- a/icons/ascii.json
+++ b/icons/ascii.json
@@ -4,6 +4,7 @@
     "like":   "*",
     "unlike": " ",
     "lock":   "$",
+    "nolock": " ",
     "empty":  " ",
     "ac":     "O",
     "notac":  "X",
diff --git a/icons/default.json b/icons/default.json
index 42bdbecd..a5263a7f 100644
--- a/icons/default.json
+++ b/icons/default.json
@@ -4,6 +4,7 @@
     "like":   "★",
     "unlike": "☆",
     "lock":   "🔒",
+    "nolock": "　",
     "empty":  " ",
     "ac":     "▣",
     "notac":  "▤",
diff --git a/icons/win7.json b/icons/win7.json
index 7f032a83..0e79a481 100644
--- a/icons/win7.json
+++ b/icons/win7.json
@@ -4,6 +4,7 @@
     "like":   "♥",
     "unlike": " ",
     "lock":   "$",
+    "nolock": " ",
     "empty":  " ",
     "ac":     "O",
     "notac":  "X",
diff --git a/lib/cache.js b/lib/cache.js
index a6097c2a..42efad0b 100644
--- a/lib/cache.js
+++ b/lib/cache.js
@@ -1,42 +1,41 @@
 'use strict';
-var fs = require('fs');
 var path = require('path');
 
-var h = require('./helper');
+var file = require('./file');
 
 const cache = {};
 
 cache.init = function() {
-  h.mkdir(h.getCacheDir());
+  file.mkdir(file.cacheDir());
 };
 
 cache.get = function(k) {
-  const fullpath = h.getCacheFile(k);
-  if (!fs.existsSync(fullpath)) return null;
+  const fullpath = file.cacheFile(k);
+  if (!file.exist(fullpath)) return null;
 
-  return JSON.parse(fs.readFileSync(fullpath));
+  return JSON.parse(file.data(fullpath));
 };
 
 cache.set = function(k, v) {
-  const fullpath = h.getCacheFile(k);
-  fs.writeFileSync(fullpath, JSON.stringify(v));
+  const fullpath = file.cacheFile(k);
+  file.write(fullpath, JSON.stringify(v));
   return true;
 };
 
 cache.del = function(k) {
-  const fullpath = h.getCacheFile(k);
-  if (!fs.existsSync(fullpath)) return false;
+  const fullpath = file.cacheFile(k);
+  if (!file.exist(fullpath)) return false;
 
-  fs.unlinkSync(fullpath);
+  file.rm(fullpath);
   return true;
 };
 
 cache.list = function() {
-  return fs.readdirSync(h.getCacheDir())
+  return file.list(file.cacheDir())
     .filter(x => path.extname(x) === '.json')
     .map(function(filename) {
       const k = path.basename(filename, '.json');
-      const stat = fs.statSync(h.getCacheFile(k));
+      const stat = file.stat(file.cacheFile(k));
       return {
         name:  k,
         size:  stat.size,
diff --git a/lib/chalk.js b/lib/chalk.js
index 090ec573..ef78e9d8 100644
--- a/lib/chalk.js
+++ b/lib/chalk.js
@@ -3,6 +3,8 @@ var _ = require('underscore');
 var style = require('ansi-styles');
 var supportsColor = require('supports-color');
 
+var file = require('./file');
+
 const chalk = {
   enabled: supportsColor.stdout,
   use256:  supportsColor.stdout && supportsColor.stdout.has256,
@@ -54,8 +56,7 @@ chalk.wrap = function(pre, post) {
 const bgName = x => 'bg' + x[0].toUpperCase() + x.substr(1);
 
 chalk.init = function() {
-  const h = require('./helper');
-  for (let f of h.getCodeDirData('colors')) {
+  for (let f of file.listCodeDir('colors')) {
     const theme = {};
     const data = _.extendOwn({}, DEFAULT, f.data);
     for (let x of _.pairs(data)) {
diff --git a/lib/cli.js b/lib/cli.js
index 051bbee2..e59cf7f5 100644
--- a/lib/cli.js
+++ b/lib/cli.js
@@ -5,6 +5,7 @@ var chalk = require('./chalk');
 var cache = require('./cache');
 var config = require('./config');
 var h = require('./helper');
+var file = require('./file');
 var icon = require('./icon');
 var log = require('./log');
 var Plugin = require('./plugin');
@@ -52,6 +53,24 @@ function initLogLevel() {
   log.setLevel(level);
 }
 
+function initDir() {
+  file.init();
+  file.mkdir(file.homeDir())
+}
+
+function initPlugins(cb) {
+  if (Plugin.init()) {
+    Plugin.save();
+    return cb();
+  } else {
+    Plugin.installMissings(function(e) {
+      if (e) return cb(e);
+      Plugin.init();
+      return cb();
+    });
+  }
+}
+
 var cli = {};
 
 function runCommand() {
@@ -73,22 +92,16 @@ cli.run = function() {
   });
 
   config.init();
-  cache.init();
 
   initColor();
   initIcon();
   initLogLevel();
-
-  if (Plugin.init()) {
-    Plugin.save();
+  initDir()
+  initPlugins(function(e) {
+    if (e) return log.fatal(e);
+    cache.init();
     runCommand();
-  } else {
-    Plugin.installMissings(function(e) {
-      if (e) return log.error(e);
-      Plugin.init();
-      runCommand();
-    });
-  }
+  });
 };
 
 module.exports = cli;
diff --git a/lib/commands/cache.js b/lib/commands/cache.js
index 30bc24b4..154a3302 100644
--- a/lib/commands/cache.js
+++ b/lib/commands/cache.js
@@ -1,12 +1,12 @@
 'use strict';
 var _ = require('underscore');
-var sprintf = require('sprintf-js').sprintf;
 
 var h = require('../helper');
 var chalk = require('../chalk');
 var log = require('../log');
 var cache = require('../cache');
 var session = require('../session');
+var sprintf = require('../sprintf');
 
 const cmd = {
   command: 'cache [keyword]',
@@ -56,8 +56,8 @@ cmd.handler = function(argv) {
       return x;
     })
     .forEach(function(f) {
-      log.printf(' %s %8s    %s ago',
-          chalk.green(sprintf('%-60s', f.name)),
+      log.printf(' %-60s %8s    %s ago',
+          chalk.green(f.name),
           h.prettySize(f.size),
           h.prettyTime((Date.now() - f.mtime) / 1000));
     });
diff --git a/lib/commands/config.js b/lib/commands/config.js
index 8d684939..6a1fe364 100644
--- a/lib/commands/config.js
+++ b/lib/commands/config.js
@@ -2,7 +2,7 @@
 var _ = require('underscore');
 var nconf = require('nconf');
 
-var h = require('../helper');
+var file = require('../file');
 var chalk = require('../chalk');
 var config = require('../config');
 var log = require('../log');
@@ -55,12 +55,12 @@ function loadConfig(showall) {
 }
 
 function saveConfig() {
-  require('fs').writeFileSync(h.getConfigFile(), prettyConfig(loadConfig(false)));
+  file.write(file.configFile(), prettyConfig(loadConfig(false)));
 }
 
 cmd.handler = function(argv) {
   session.argv = argv;
-  nconf.file('local', h.getConfigFile());
+  nconf.file('local', file.configFile());
 
   // show all
   if (argv.key.length === 0)
@@ -75,14 +75,14 @@ cmd.handler = function(argv) {
 
   // delete
   if (argv.delete) {
-    if (v === undefined) return log.error('Key not found: ' + argv.key);
+    if (v === undefined) return log.fatal('Key not found: ' + argv.key);
     nconf.clear(argv.key);
     return saveConfig();
   }
 
   // show
   if (argv.value.length === 0) {
-    if (v === undefined) return log.error('Key not found: ' + argv.key);
+    if (v === undefined) return log.fatal('Key not found: ' + argv.key);
     return log.info(prettyConfig(v));
   }
 
diff --git a/lib/commands/list.js b/lib/commands/list.js
index f3e69143..c010de86 100644
--- a/lib/commands/list.js
+++ b/lib/commands/list.js
@@ -1,6 +1,5 @@
 'use strict';
 var _ = require('underscore');
-var sprintf = require('sprintf-js').sprintf;
 
 var h = require('../helper');
 var chalk = require('../chalk');
@@ -67,14 +66,14 @@ cmd.handler = function(argv) {
       if (problem.locked) ++stat.locked;
       if (problem.starred) ++stat.starred;
 
-      log.printf('%s %s %s [%3d] %-60s %-6s (%.2f %%)',
+      log.printf('%s %s %s [%=4s] %-60s %-6s (%s %%)',
           (problem.starred ? chalk.yellow(icon.like) : icon.empty),
-          (problem.locked ? chalk.red(icon.lock) : icon.empty),
+          (problem.locked ? chalk.red(icon.lock) : icon.nolock),
           h.prettyState(problem.state),
           problem.fid,
           problem.name,
-          h.prettyLevel(sprintf('%-6s', problem.level)),
-          problem.percent);
+          h.prettyLevel(problem.level),
+          problem.percent.toFixed(2));
 
       if (argv.extra) {
         let badges = [problem.category];
@@ -99,9 +98,9 @@ cmd.handler = function(argv) {
 
     if (argv.stat) {
       log.info();
-      log.printf('      Listed: %-9d Locked:  %-9d Starred: %-9d', problems.length, stat.locked, stat.starred);
-      log.printf('      Accept: %-9d Not-AC:  %-9d Remain:  %-9d', stat.ac, stat.notac, stat.None);
-      log.printf('      Easy:   %-9d Medium:  %-9d Hard:    %-9d', stat.Easy, stat.Medium, stat.Hard);
+      log.printf('      Listed: %-9s Locked:  %-9s Starred: %-9s', problems.length, stat.locked, stat.starred);
+      log.printf('      Accept: %-9s Not-AC:  %-9s Remain:  %-9s', stat.ac, stat.notac, stat.None);
+      log.printf('      Easy:   %-9s Medium:  %-9s Hard:    %-9s', stat.Easy, stat.Medium, stat.Hard);
     }
   });
 };
diff --git a/lib/commands/plugin.js b/lib/commands/plugin.js
index 6a5b9721..78a966b5 100644
--- a/lib/commands/plugin.js
+++ b/lib/commands/plugin.js
@@ -1,12 +1,11 @@
 'use strict';
-var sprintf = require('sprintf-js').sprintf;
-
 var h = require('../helper');
 var chalk = require('../chalk');
 var config = require('../config');
 var log = require('../log');
 var Plugin = require('../plugin');
 var session = require('../session');
+var sprintf = require('../sprintf');
 
 const cmd = {
   command: 'plugin [name]',
@@ -70,7 +69,6 @@ function print(plugins) {
     log.printf('   %s     %-18s %-15s %s',
       h.prettyText('', p.enabled && !p.missing),
       p.name, p.ver, p.desc);
-  Plugin.save();
 }
 
 cmd.handler = function(argv) {
@@ -80,8 +78,10 @@ cmd.handler = function(argv) {
   const name = argv.name;
 
   if (argv.install) {
-    const cb = function(e) {
-      if (e) return log.error(e);
+    const cb = function(e, p) {
+      if (e) return log.fatal(e);
+      p.help();
+      p.save();
       Plugin.init();
       print();
     };
@@ -95,18 +95,18 @@ cmd.handler = function(argv) {
   }
 
   if (name) plugins = plugins.filter(x => x.name === name);
-  if (plugins.length === 0) return log.error('Plugin not found!');
+  if (plugins.length === 0) return log.fatal('Plugin not found!');
 
   const p = plugins[0];
   if (p.missing && (argv.enable || argv.disable))
-    return log.error('Plugin missing, install it first');
+    return log.fatal('Plugin missing, install it first');
 
   if (argv.enable) {
-    p.enable(true);
+    p.enabled = true;
     p.save();
     print();
   } else if (argv.disable) {
-    p.enable(false);
+    p.enabled = false;
     p.save();
     print();
   } else if (argv.delete) {
diff --git a/lib/commands/session.js b/lib/commands/session.js
index d348dea5..64d460d6 100644
--- a/lib/commands/session.js
+++ b/lib/commands/session.js
@@ -1,12 +1,12 @@
 'use strict';
 var prompt = require('prompt');
-var sprintf = require('sprintf-js').sprintf;
 
 var h = require('../helper');
 var chalk = require('../chalk');
 var log = require('../log');
 var core = require('../core');
 var session = require('../session');
+var sprintf = require('../sprintf');
 
 const cmd = {
   command: 'session [keyword]',
@@ -61,14 +61,14 @@ function printSessions(e, sessions) {
     if (s.total_submitted > 0)
       submissionRate = s.total_acs * 100 / s.total_submitted;
 
-    log.printf('   %s   %8d   %-26s %s (%6s %%) %s (%6s %%)',
+    log.printf('   %s   %8s   %-26s %6s (%6s %%) %6s (%6s %%)',
       s.is_active ? h.prettyState('ac') : ' ',
       s.id,
       s.name || 'Anonymous Session',
-      chalk.green(sprintf('%6s', s.ac_questions)),
-      sprintf('%.2f', questionRate),
-      chalk.green(sprintf('%6s', s.total_acs)),
-      sprintf('%.2f', submissionRate));
+      chalk.green(s.ac_questions),
+      questionRate.toFixed(2),
+      chalk.green(s.total_acs),
+      submissionRate.toFixed(2));
   }
 }
 
diff --git a/lib/commands/show.js b/lib/commands/show.js
index d930740f..c3bf9977 100644
--- a/lib/commands/show.js
+++ b/lib/commands/show.js
@@ -1,11 +1,11 @@
 'use strict';
-var fs = require('fs');
 var util = require('util');
 
 var _ = require('underscore');
 var childProcess = require('child_process');
 
 var h = require('../helper');
+var file = require('../file');
 var chalk = require('../chalk');
 var icon = require('../icon');
 var log = require('../log');
@@ -57,6 +57,18 @@ const cmd = {
         default:  false,
         describe: 'Show extra question details in source code'
       })
+      .option('getRangeEnd', {
+        alias:    'getRangeEnd',
+        type:     'string',
+        default:  false,
+        describe: 'End number of question range'
+      })
+      .option('getRangeStart', {
+        alias:    'getRangeStart',
+        type:     'string',
+        default:  false,
+        describe: 'Start number of question range'
+      })
       .positional('keyword', {
         type:     'string',
         default:  '',
@@ -75,20 +87,18 @@ const cmd = {
 function genFileName(problem, opts) {
   const path = require('path');
   const params = [
-    problem.fid,
-    problem.slug,
+    file.fmt(config.file.show, problem),
     '',
     h.langToExt(opts.lang)
   ];
 
-  // try to use a new filename to avoid overwrite by mistake
-  let i = 0;
-  let name;
-  do {
-    name = path.join(opts.outdir, params.join('.').replace(/\.+/g, '.'));
-    params[2] = i++;
-  } while (fs.existsSync(name));
-  return name;
+  // try new name to avoid overwrite by mistake
+  for (let i = 0; ; ++i) {
+    const name = path.join(opts.outdir, params.join('.').replace(/\.+/g, '.'));
+    if (!file.exist(name))
+      return name;
+    params[1] = i;
+  }
 }
 
 function showProblem(problem, argv) {
@@ -122,9 +132,9 @@ function showProblem(problem, argv) {
 
   let filename;
   if (argv.gen) {
+    file.mkdir(argv.outdir);
     filename = genFileName(problem, argv);
-    h.mkdir(argv.outdir);
-    fs.writeFileSync(filename, code);
+    file.write(filename, code);
 
     if (argv.editor !== undefined) {
       childProcess.spawn(argv.editor || config.code.editor, [filename], {
@@ -139,7 +149,7 @@ function showProblem(problem, argv) {
     }
   }
 
-  log.printf('[%d] %s %s', problem.fid, problem.name,
+  log.printf('[%s] %s %s', problem.fid, problem.name,
       (problem.starred ? chalk.yellow(icon.like) : icon.empty));
   log.info();
   log.info(chalk.underline(problem.link));
@@ -152,7 +162,7 @@ function showProblem(problem, argv) {
 
   log.info();
   log.printf('* %s', problem.category);
-  log.printf('* %s (%.2f%%)', h.prettyLevel(problem.level), problem.percent);
+  log.printf('* %s (%s%%)', h.prettyLevel(problem.level), problem.percent.toFixed(2));
 
   if (filename)
     log.printf('* Source Code:       %s', chalk.yellow.underline(filename));
@@ -169,32 +179,49 @@ function showProblem(problem, argv) {
 
 cmd.handler = function(argv) {
   session.argv = argv;
-  if (argv.keyword.length > 0) {
-    // show specific one
-    core.getProblem(argv.keyword, function(e, problem) {
-      if (e) return log.fail(e);
-      showProblem(problem, argv);
-    });
+  let end = (Number.parseInt(argv.getRangeEnd) >= 1) ? Number.parseInt(argv.getRangeEnd) : 1
+  let start = (Number.parseInt(argv.getRangeStart) >= 1) ? Number.parseInt(argv.getRangeStart) : 1
+  if (start > end) { start = end}
+  if (end) {
+    let i = start
+    while (i <= end) {
+      (function(i) {
+        setTimeout(function() {
+          core.getProblem(i, function(e, problem) {
+            if (e) { return log.fail(e); }
+            showProblem(problem, argv);
+          })
+        }, 50 * (i - start))
+      })(i++)
+    }
   } else {
-    // show random one
-    core.filterProblems(argv, function(e, problems) {
-      if (e) return log.fail(e);
-
-      // random select one that not AC-ed yet
-      const user = session.getUser();
-      problems = problems.filter(function(x) {
-        if (x.state === 'ac') return false;
-        if (!user.paid && x.locked) return false;
-        return true;
-      });
-      if (problems.length === 0) return log.fail('Problem not found!');
-
-      const problem = _.sample(problems);
-      core.getProblem(problem, function(e, problem) {
+    if (argv.keyword.length > 0) {
+      // show specific one
+      core.getProblem(argv.keyword, function(e, problem) {
         if (e) return log.fail(e);
         showProblem(problem, argv);
       });
-    });
+    } else {
+      // show random one
+      core.filterProblems(argv, function(e, problems) {
+        if (e) return log.fail(e);
+
+        // random select one that not AC-ed yet
+        const user = session.getUser();
+        problems = problems.filter(function(x) {
+          if (x.state === 'ac') return false;
+          if (!user.paid && x.locked) return false;
+          return true;
+        });
+        if (problems.length === 0) return log.fail('Problem not found!');
+
+        const problem = _.sample(problems);
+        core.getProblem(problem, function(e, problem) {
+          if (e) return log.fail(e);
+          showProblem(problem, argv);
+        });
+      });
+    }
   }
 };
 
diff --git a/lib/commands/star.js b/lib/commands/star.js
index 991e10e4..3660432b 100644
--- a/lib/commands/star.js
+++ b/lib/commands/star.js
@@ -35,7 +35,7 @@ cmd.handler = function(argv) {
     core.starProblem(problem, !argv.delete, function(e, starred) {
       if (e) return log.fail(e);
 
-      log.printf('[%d] %s %s', problem.fid, problem.name,
+      log.printf('[%s] %s %s', problem.fid, problem.name,
           chalk.yellow(starred ? icon.like : icon.unlike));
 
       core.updateProblem(problem, {starred: starred});
diff --git a/lib/commands/stat.js b/lib/commands/stat.js
index 54f7a41a..3c51ab98 100644
--- a/lib/commands/stat.js
+++ b/lib/commands/stat.js
@@ -1,6 +1,5 @@
 'use strict';
 var moment = require('moment');
-var sprintf = require('sprintf-js').sprintf;
 var _ = require('underscore');
 
 var chalk = require('../chalk');
@@ -8,6 +7,7 @@ var icon = require('../icon');
 var log = require('../log');
 var core = require('../core');
 var session = require('../session');
+var sprintf = require('../sprintf');
 var h = require('../helper');
 
 const cmd = {
@@ -50,9 +50,9 @@ function printLine(key, done, all) {
   const n = 30;
   const percent = (all > 0) ? done / all : 0;
   const x = Math.ceil(n * percent);
-  log.printf(' %s\t%3d/%-3d (%6s %%)  %s%s',
+  log.printf(' %s\t%3s/%-3s (%6s %%)  %s%s',
     h.prettyLevel(key), done, all,
-    sprintf('%.2f', 100 * percent),
+    (100 * percent).toFixed(2),
     chalk.green('█'.repeat(x)),
     chalk.red('░'.repeat(n - x)));
 }
@@ -96,7 +96,7 @@ function showGraph(problems) {
   if (groups > 5) groups = 5;
 
   const header = _.range(groups)
-    .map(x => sprintf('%4d%18d', x * 10 + 1, x * 10 + 10))
+    .map(x => sprintf('%4s%18s', x * 10 + 1, x * 10 + 10))
     .join('');
   log.info('     ' + header);
 
@@ -104,7 +104,7 @@ function showGraph(problems) {
   for (let problem of problems)
     graph[problem.fid] = ICONS[problem.state] || ICONS.none;
 
-  let line = [sprintf(' %03d', 0)];
+  let line = [sprintf(' %03s', 0)];
   for (let i = 1, n = graph.length; i <= n; ++i) {
     // padding before group
     if (i % 10 === 1) line.push(' ');
@@ -114,7 +114,7 @@ function showGraph(problems) {
     // time to start new row
     if (i % (10 * groups) === 0 || i === n) {
       log.info(line.join(' '));
-      line = [sprintf(' %03d', i)];
+      line = [sprintf(' %03s', i)];
     }
   }
 
diff --git a/lib/commands/submission.js b/lib/commands/submission.js
index ff570674..de0449a3 100644
--- a/lib/commands/submission.js
+++ b/lib/commands/submission.js
@@ -1,10 +1,12 @@
 'use strict';
-var fs = require('fs');
+var path = require('path');
 
-var sprintf = require('sprintf-js').sprintf;
+var _ = require('underscore');
 
 var h = require('../helper');
+var file = require('../file');
 var chalk = require('../chalk');
+var config = require('../config');
 var log = require('../log');
 var Queue = require('../queue');
 var core = require('../core');
@@ -60,7 +62,7 @@ function doTask(problem, queue, cb) {
     // - green: accepted, fresh download
     // - yellow: not ac-ed, fresh download
     // - white: existed already, skip download
-    log.printf('[%3d] %-60s %s', problem.fid, problem.name,
+    log.printf('[%=4s] %-60s %s', problem.fid, problem.name,
         (e ? chalk.red('ERROR: ' + (e.msg || e)) : msg));
     if (cb) cb(e);
   }
@@ -91,17 +93,15 @@ function exportSubmission(problem, argv, cb) {
     const submission = submissions.find(x => x.status_display === 'Accepted') || submissions[0];
     submission.ac = (submission.status_display === 'Accepted');
 
-    const f = sprintf('%s/%d.%s.%s.%s%s',
-        argv.outdir,
-        problem.fid,
-        problem.slug,
-        submission.id,
-        submission.ac ? 'ac' : 'notac',
-        h.langToExt(submission.lang));
+    const data = _.extend({}, submission, problem);
+    data.sid = submission.id;
+    data.ac = submission.ac ? 'ac' : 'notac';
+    const basename = file.fmt(config.file.submission, data);
+    const f = path.join(argv.outdir, basename + h.langToExt(submission.lang));
 
-    h.mkdir(argv.outdir);
+    file.mkdir(argv.outdir);
     // skip the existing cached submissions
-    if (fs.existsSync(f))
+    if (file.exist(f))
       return cb(null, chalk.underline(f));
 
     core.getSubmission(submission, function(e, submission) {
@@ -112,7 +112,7 @@ function exportSubmission(problem, argv, cb) {
         code: submission.code,
         tpl:  argv.extra ? 'detailed' : 'codeonly'
       };
-      fs.writeFileSync(f, core.exportProblem(problem, opts));
+      file.write(f, core.exportProblem(problem, opts));
       cb(null, submission.ac ? chalk.green.underline(f)
                              : chalk.yellow.underline(f));
     });
diff --git a/lib/commands/submit.js b/lib/commands/submit.js
index 297084f7..7376598b 100644
--- a/lib/commands/submit.js
+++ b/lib/commands/submit.js
@@ -1,8 +1,8 @@
 'use strict';
-var fs = require('fs');
 var util = require('util');
 
 var h = require('../helper');
+var file = require('../file');
 var chalk = require('../chalk');
 var log = require('../log');
 var core = require('../core');
@@ -43,17 +43,16 @@ function printLine() {
 
 cmd.handler = function(argv) {
   session.argv = argv;
-  if (!fs.existsSync(argv.filename))
-    return log.error('File ' + argv.filename + ' not exist!');
+  if (!file.exist(argv.filename))
+    return log.fatal('File ' + argv.filename + ' not exist!');
 
-  // use the 1st section in filename as keyword
-  // e.g. two-sum.cpp, or two-sum.78502271.ac.cpp
-  const keyword = h.getFilename(argv.filename).split('.')[0];
+  const meta = file.meta(argv.filename);
 
-  core.getProblem(keyword, function(e, problem) {
+  core.getProblem(meta.id, function(e, problem) {
     if (e) return log.fail(e);
 
     problem.file = argv.filename;
+    problem.lang = meta.lang;
 
     core.submitProblem(problem, function(e, results) {
       if (e) return log.fail(e);
diff --git a/lib/commands/test.js b/lib/commands/test.js
index ff320548..21c4a4eb 100644
--- a/lib/commands/test.js
+++ b/lib/commands/test.js
@@ -1,8 +1,8 @@
 'use strict';
-var fs = require('fs');
 var _ = require('underscore');
 
 var h = require('../helper');
+var file = require('../file');
 var chalk = require('../chalk');
 var log = require('../log');
 var core = require('../core');
@@ -53,14 +53,12 @@ function printResult(actual, expect, k) {
 }
 
 function runTest(argv) {
-  if (!fs.existsSync(argv.filename))
-    return log.error('File ' + argv.filename + ' not exist!');
+  if (!file.exist(argv.filename))
+    return log.fatal('File ' + argv.filename + ' not exist!');
 
-  // use the 1st section in filename as keyword
-  // e.g. two-sum.cpp, or two-sum.78502271.ac.cpp
-  const keyword = h.getFilename(argv.filename).split('.')[0];
+  const meta = file.meta(argv.filename);
 
-  core.getProblem(keyword, function(e, problem) {
+  core.getProblem(meta.id, function(e, problem) {
     if (e) return log.fail(e);
 
     if (!problem.testable)
@@ -73,6 +71,7 @@ function runTest(argv) {
       return log.fail('missing testcase?');
 
     problem.file = argv.filename;
+    problem.lang = meta.lang;
 
     log.info('\nInput data:');
     log.info(problem.testcase);
diff --git a/lib/commands/user.js b/lib/commands/user.js
index af9ea4c2..4cd903cd 100644
--- a/lib/commands/user.js
+++ b/lib/commands/user.js
@@ -1,6 +1,5 @@
 'use strict';
 var prompt = require('prompt');
-var sprintf = require('sprintf-js').sprintf;
 
 var h = require('../helper');
 var config = require('../config');
@@ -8,6 +7,7 @@ var chalk = require('../chalk');
 var log = require('../log');
 var core = require('../core');
 var session = require('../session');
+var sprintf = require('../sprintf');
 
 const cmd = {
   command: 'user',
@@ -65,9 +65,9 @@ cmd.handler = function(argv) {
     if (user) {
       log.info(chalk.gray(sprintf(' %-9s %-20s %s', 'Premium', 'User', 'Host')));
       log.info(chalk.gray('-'.repeat(60)));
-      log.printf('    %s      %s %s',
+      log.printf('    %s      %-20s %s',
         h.prettyText('', user.paid || false),
-        chalk.yellow(sprintf('%-20s', user.name)),
+        chalk.yellow(user.name),
         config.sys.urls.base);
     } else
       return log.fail('You are not login yet?');
diff --git a/lib/commands/version.js b/lib/commands/version.js
index 684166a7..4ba16749 100644
--- a/lib/commands/version.js
+++ b/lib/commands/version.js
@@ -1,6 +1,7 @@
 'use strict';
 var _ = require('underscore');
 
+var file = require('../file');
 var chalk = require('../chalk');
 var icon = require('../icon');
 var log = require('../log');
@@ -46,19 +47,18 @@ cmd.handler = function(argv) {
     '| | ___  ___| |_  ___  ___   __| | ___ ',
     '| |/ _ \\/ _ \\ __|/ __|/ _ \\ / _` |/ _ \\',
     '| |  __/  __/ |_  (__| (_) | (_| |  __/',
-    '|_|\\___|\\___|\\__|\\___|\\___/ \\__,_|\\___|  CLI v' + version
+    '|_|\\___|\\___|\\__|\\___|\\___/ \\__,_|\\___|  CLI ' + chalk.green('v' + version)
   ].join('\n');
   log.info(logo);
 
-  const h = require('../helper');
   const os = require('os');
   const config = require('../config');
 
   log.info('\n[Environment]');
   printLine('Node', process.version);
   printLine('OS', os.platform() + ' ' + os.release());
-  printLine('Cache', h.getCacheDir());
-  printLine('Config', h.getConfigFile());
+  printLine('Cache', file.cacheDir());
+  printLine('Config', file.configFile());
 
   log.info('\n[Configuration]');
   _.each(config.getAll(true), function(v, k) {
diff --git a/lib/config.js b/lib/config.js
index 1f53cce9..692b958d 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -2,7 +2,7 @@
 var _ = require('underscore');
 var nconf = require('nconf');
 
-var h = require('./helper');
+var file = require('./file');
 
 const DEFAULT_CONFIG = {
   // usually you don't wanna change those
@@ -55,6 +55,10 @@ const DEFAULT_CONFIG = {
     editor: 'vim',
     lang:   'cpp'
   },
+  file: {
+    show:       '${fid}.${slug}',
+    submission: '${fid}.${slug}.${sid}.${ac}'
+  },
   color: {
     enable: true,
     theme:  'default'
@@ -71,7 +75,7 @@ const DEFAULT_CONFIG = {
 function Config() {}
 
 Config.prototype.init = function() {
-  nconf.file('local', h.getConfigFile())
+  nconf.file('local', file.configFile())
     .add('global', {type: 'literal', store: DEFAULT_CONFIG})
     .defaults({});
 
diff --git a/lib/core.js b/lib/core.js
index a1356a62..74362f78 100644
--- a/lib/core.js
+++ b/lib/core.js
@@ -1,11 +1,11 @@
 'use strict';
-var path = require('path');
 var util = require('util');
 
 var _ = require('underscore');
 
 var log = require('./log');
 var h = require('./helper');
+var file = require('./file');
 var Plugin = require('./plugin');
 
 const core = new Plugin(99999999, 'core', '20170722', 'Plugins manager');
@@ -106,31 +106,26 @@ core.starProblem = function(problem, starred, cb) {
 };
 
 core.exportProblem = function(problem, opts) {
-  // copy problem attrs thus we can render it in template
-  const input = _.extend({}, problem);
+  const data = _.extend({}, problem);
 
-  input.code = opts.code.replace(/\r\n/g, '\n');
-  input.comment = h.langToCommentStyle(opts.lang);
-  input.percent = input.percent.toFixed(2);
-  input.testcase = util.inspect(input.testcase || '');
+  // unify format before rendering
+  data.app = require('./config').app || 'leetcode';
+  if (!data.fid) data.fid = data.id;
+  if (!data.lang) data.lang = opts.lang;
+  data.code = (opts.code || data.code || '').replace(/\r\n/g, '\n');
+  data.comment = h.langToCommentStyle(data.lang);
+  data.percent = data.percent.toFixed(2);
+  data.testcase = util.inspect(data.testcase || '');
 
   if (opts.tpl === 'detailed') {
     // NOTE: wordwrap internally uses '\n' as EOL, so here we have to
     // remove all '\r' in the raw string.
-    const desc = input.desc.replace(/\r\n/g, '\n').replace(/^ /mg, '⁠');
-    const wrap = require('wordwrap')(79 - input.comment.line.length);
-    input.desc = wrap(desc).split('\n');
+    const desc = data.desc.replace(/\r\n/g, '\n').replace(/^ /mg, '⁠');
+    const wrap = require('wordwrap')(79 - data.comment.line.length);
+    data.desc = wrap(desc).split('\n');
   }
 
-  const tplfile = path.join(h.getCodeDir('templates'), opts.tpl + '.tpl');
-  let output = _.template(h.getFileData(tplfile))(input);
-
-  if (h.isWindows()) {
-    output = output.replace(/\n/g, '\r\n');
-  } else {
-    output = output.replace(/\r\n/g, '\n');
-  }
-  return output;
+  return file.render(opts.tpl, data);
 };
 
 module.exports = core;
diff --git a/lib/file.js b/lib/file.js
new file mode 100644
index 00000000..51ea21c3
--- /dev/null
+++ b/lib/file.js
@@ -0,0 +1,166 @@
+'use strict';
+var fs = require('fs');
+var path = require('path');
+
+var _ = require('underscore');
+var mkdirp = require('mkdirp');
+
+const file = {}
+
+file.init = function() {
+  _.templateSettings = {
+    evaluate:    /\{\{(.+?)\}\}/g,
+    interpolate: /\$\{(.+?)\}/g
+  };
+};
+
+file.isWindows = function() {
+  return process.platform === 'win32';
+};
+
+/// app dirs ///
+file.userHomeDir = function() {
+  return process.env.HOME || process.env.USERPROFILE;
+};
+
+file.homeDir = function() {
+  return path.join(this.userHomeDir(), '.lc');
+};
+
+file.appDir = function() {
+  const config = require('./config');
+  return path.join(this.homeDir(), config.app || 'leetcode');
+};
+
+file.cacheDir = function() {
+  return path.join(this.appDir(), 'cache');
+};
+
+file.codeDir = function(dir) {
+  return path.join(__dirname, '..', dir || '');
+};
+
+/// app files ///
+file.cacheFile = function(k) {
+  return path.join(this.cacheDir(), k + '.json');
+};
+
+file.configFile = function() {
+  return path.join(this.homeDir(), 'config.json');
+};
+
+file.pluginFile = function(name) {
+  return path.join(this.codeDir('lib/plugins'), path.basename(name));
+};
+
+file.listCodeDir = function(dir) {
+  dir = this.codeDir(dir);
+  return this.list(dir).map(function(f) {
+    const fullpath = path.join(dir, f);
+    const ext = path.extname(f);
+    const name = path.basename(f, ext);
+
+    let data = null;
+    switch (ext) {
+      case '.js':   data = require(fullpath); break;
+      case '.json': data = JSON.parse(file.data(fullpath)); break;
+    }
+    return {name: name, data: data, file: f};
+  });
+};
+
+/// general dirs & files ///
+file.mkdir = function(fullpath) {
+  if (fs.existsSync(fullpath)) return;
+  mkdirp.sync(fullpath);
+};
+
+file.exist = function(fullpath) {
+  return fs.existsSync(fullpath);
+};
+
+file.rm = function(fullpath) {
+  return fs.unlinkSync(fullpath);
+};
+
+file.mv = function(src, dst) {
+  return fs.renameSync(src, dst);
+};
+
+file.list = function(dir) {
+  return fs.readdirSync(dir);
+};
+
+file.stat = function(fullpath) {
+  return fs.statSync(fullpath);
+};
+
+file.write = function(fullpath, data) {
+  return fs.writeFileSync(fullpath, data);
+};
+
+file.name = function(fullpath) {
+  return path.basename(fullpath, path.extname(fullpath));
+};
+
+file.data = function(fullpath) {
+  return fs.existsSync(fullpath) ? fs.readFileSync(fullpath).toString() : null;
+};
+
+/// templates & metadata ///
+file.render = function(tpl, data) {
+  const tplfile = path.join(this.codeDir('templates'), tpl + '.tpl');
+  let result = _.template(this.data(tplfile).replace(/\r\n/g, '\n'))(data);
+
+  if (this.isWindows()) {
+    result = result.replace(/\n/g, '\r\n');
+  } else {
+    result = result.replace(/\r\n/g, '\n');
+  }
+  return result;
+};
+
+file.fmt = function(format, data) {
+  return _.template(format)(data);
+};
+
+file.metaByName = function(filename) {
+  const m = {};
+
+  // expect the 1st section in filename as id
+  // e.g. 1.two-sum.cpp
+  m.id = file.name(filename).split('.')[0];
+
+  // HACK: compatible with old ext
+  if (filename.endsWith('.py3') || filename.endsWith('.python3.py'))
+    m.lang = 'python3';
+  else
+    m.lang = require('./helper').extToLang(filename);
+
+  return m;
+};
+
+file.meta = function(filename) {
+  const m = {};
+
+  // first look into the file data
+  const line = this.data(filename).split('\n')
+    .find(x => x.indexOf(' @lc ') >= 0) || '';
+  line.split(' ').forEach(function(x) {
+    const v = x.split('=');
+    if (v.length == 2) {
+      m[v[0]] = v[1].trim();
+    }
+  });
+
+  // otherwise, look into file name
+  if (!m.id || !m.lang) {
+    const olddata = this.metaByName(filename);
+    m.id = m.id || olddata.id;
+    m.lang = m.lang || olddata.lang;
+  }
+
+  return m;
+};
+
+module.exports = file;
diff --git a/lib/helper.js b/lib/helper.js
index 33b9e1f3..5ca496aa 100644
--- a/lib/helper.js
+++ b/lib/helper.js
@@ -1,11 +1,9 @@
 'use strict';
-var fs = require('fs');
-var path = require('path');
-
 var _ = require('underscore');
-var mkdirp = require('mkdirp');
 var ora = require('ora');
 
+var file = require('./file');
+
 const UNITS_SIZE = [
   {unit: 'B', name: 'Bytes',  count: 1024},
   {unit: 'K', name: 'KBytes', count: 1024},
@@ -40,9 +38,9 @@ const LANGS = [
   {lang: 'java',       ext: '.java',       style: 'c'},
   {lang: 'javascript', ext: '.js',         style: 'c'},
   {lang: 'kotlin',     ext: '.kt',         style: 'c'},
-  {lang: 'mysql',      ext: '.sql',        style: '#'},
+  {lang: 'mysql',      ext: '.sql',        style: '--'},
   {lang: 'python',     ext: '.py',         style: '#'},
-  {lang: 'python3',    ext: '.python3.py', style: '#'},
+  {lang: 'python3',    ext: '.py',         style: '#'},
   {lang: 'ruby',       ext: '.rb',         style: '#'},
   {lang: 'scala',      ext: '.scala',      style: 'c'},
   {lang: 'swift',      ext: '.swift',      style: 'c'}
@@ -53,15 +51,11 @@ const h = {};
 h.KEYS = {
   user:     '../user',
   stat:     '../stat',
-  plugins:  '../plugins',
+  plugins:  '../../plugins',
   problems: 'problems',
   problem:  p => p.fid + '.' + p.slug + '.' + p.category
 };
 
-h.isWindows = function() {
-  return process.platform === 'win32';
-};
-
 h.prettyState = function(state) {
   switch (state) {
     case 'ac':    return this.prettyText('', true);
@@ -130,80 +124,16 @@ h.langToExt = function(lang) {
 };
 
 h.extToLang = function(fullpath) {
-  // HACK: compatible with old ext
-  if (fullpath.endsWith('.py3')) return 'python3';
-
-  const res = _.chain(LANGS)
-    .filter(x => fullpath.endsWith(x.ext))
-    .sortBy(x => -x.ext.length)
-    .value();
-  return res.length ? res[0].lang : 'unknown';
+  const res = LANGS.find(x => fullpath.endsWith(x.ext));
+  return res ? res.lang : 'unknown';
 };
 
 h.langToCommentStyle = function(lang) {
   const res = LANGS.find(x => x.lang === lang);
 
-  return (res && res.style === '#') ?
-  {start: '#',  line: '#',  end: '#'} :
-  {start: '/*', line: ' *', end: ' */'};
-};
-
-h.mkdir = function(fullpath) {
-  if (fs.existsSync(fullpath)) return;
-  mkdirp.sync(fullpath);
-};
-
-h.getCodeDirData = function(dir) {
-  dir = h.getCodeDir(dir);
-  return fs.readdirSync(dir).map(function(file) {
-    const fullpath = path.join(dir, file);
-    const ext = path.extname(file);
-
-    const name = path.basename(file, ext);
-    let data = null;
-
-    switch (ext) {
-      case '.js':   data = require(fullpath); break;
-      case '.json': data = JSON.parse(h.getFileData(fullpath)); break;
-    }
-    return {name: name, data: data, file: file};
-  });
-};
-
-h.getFilename = function(fullpath) {
-  return path.basename(fullpath, path.extname(fullpath));
-};
-
-h.getFileData = function(fullpath) {
-  return fs.existsSync(fullpath) ? fs.readFileSync(fullpath).toString() : null;
-};
-
-h.getUserHomeDir = function() {
-  return process.env.HOME || process.env.USERPROFILE;
-};
-
-h.getHomeDir = function() {
-  return path.join(this.getUserHomeDir(), '.lc');
-};
-
-h.getCacheDir = function() {
-  return path.join(this.getHomeDir(), 'cache');
-};
-
-h.getCodeDir = function(dir) {
-  return path.join(__dirname, '..', dir || '');
-};
-
-h.getCacheFile = function(k) {
-  return path.join(this.getCacheDir(), k + '.json');
-};
-
-h.getConfigFile = function() {
-  return path.join(this.getHomeDir(), 'config.json');
-};
-
-h.getPluginFile = function(name) {
-  return path.join(this.getCodeDir('lib/plugins'), path.basename(name));
+  return (res && res.style === 'c') ?
+    {start: '/*', line: ' *', end: ' */'} :
+    {start: res.style,  line: res.style,  end: res.style};
 };
 
 h.readStdin = function(cb) {
@@ -211,13 +141,13 @@ h.readStdin = function(cb) {
   const bufs = [];
 
   console.log('NOTE: to finish the input, press ' +
-      (this.isWindows() ? '<Ctrl-D> and <Return>' : '<Ctrl-D>'));
+      (file.isWindows() ? '<Ctrl-D> and <Return>' : '<Ctrl-D>'));
 
   stdin.on('readable', function() {
     const data = stdin.read();
     if (data) {
       // windows doesn't treat ctrl-D as EOF
-      if (h.isWindows() && data.toString() === '\x04\r\n') {
+      if (file.isWindows() && data.toString() === '\x04\r\n') {
         stdin.emit('end');
       } else {
         bufs.push(data);
diff --git a/lib/icon.js b/lib/icon.js
index af95137b..c147a792 100644
--- a/lib/icon.js
+++ b/lib/icon.js
@@ -1,7 +1,7 @@
 'use strict';
 var _ = require('underscore');
 
-var h = require('./helper');
+var file = require('./file');
 
 const icons = {
   yes:    '✔',
@@ -18,13 +18,13 @@ const icons = {
 };
 
 icons.setTheme = function(name) {
-  const defaultName = h.isWindows() ? 'win7' : 'default';
+  const defaultName = file.isWindows() ? 'win7' : 'default';
   const theme = this.themes.get(name) || this.themes.get(defaultName) || {};
   _.extendOwn(this, theme);
 };
 
 icons.init = function() {
-  for (let f of h.getCodeDirData('icons'))
+  for (let f of file.listCodeDir('icons'))
     icons.themes.set(f.name, f.data);
 };
 
diff --git a/lib/log.js b/lib/log.js
index cb8435de..394b356c 100644
--- a/lib/log.js
+++ b/lib/log.js
@@ -1,8 +1,8 @@
 'use strict';
 var _ = require('underscore');
-var sprintf = require('sprintf-js').sprintf;
 
 var chalk = require('./chalk');
+var sprintf = require('./sprintf');
 
 const log = {
   output: _.bind(console.log, console),
@@ -25,7 +25,16 @@ log.isEnabled = function(name) {
 };
 
 log.fail = function(e) {
-  log.error(sprintf('%s [%d]', (e.msg || e), (e.statusCode || 0)));
+  let msg = sprintf('%s', (e.msg || e));
+  if (e.statusCode) {
+    msg += sprintf(' [code=%s]', e.statusCode);
+  }
+  log.error(msg);
+};
+
+log.fatal = function(e) {
+  log.error(e);
+  process.exit(1);
 };
 
 log.printf = function() {
diff --git a/lib/plugin.js b/lib/plugin.js
index 5f430ebf..bbd6da44 100644
--- a/lib/plugin.js
+++ b/lib/plugin.js
@@ -7,6 +7,7 @@ var _ = require('underscore');
 var request = require('request');
 
 var h = require('./helper');
+var file = require('./file');
 var cache = require('./cache');
 var config = require('./config');
 var log = require('./log');
@@ -20,7 +21,8 @@ function Plugin(id, name, ver, desc, deps) {
 
   this.enabled = true;
   this.deleted = false;
-  this.missing = (ver === 'missing');
+  this.missing = (this.ver === 'missing');
+  this.builtin = (this.ver === 'default');
 
   // only need deps for current platform
   this.deps = _.chain(deps || [])
@@ -39,27 +41,11 @@ Plugin.prototype.setNext = function(next) {
   this.next = next;
 };
 
-Plugin.prototype.setFile = function(file) {
-  this.file = file;
-  this.enabled = (file[0] !== '.');
-};
-
-Plugin.prototype.enable = function(enabled) {
-  if (this.enabled === enabled) return;
-  const newfile = enabled ? this.file.substr(1) : '.' + this.file;
-  try {
-    fs.renameSync(h.getPluginFile(this.file), h.getPluginFile(newfile));
-  } catch(e) {
-    log.error(e.message);
-  }
-  this.setFile(newfile);
-};
-
 Plugin.prototype.delete = function() {
   if (!this.missing) {
     try {
-      const fullpath = h.getPluginFile(this.file);
-      fs.unlinkSync(fullpath);
+      const fullpath = file.pluginFile(this.file);
+      file.rm(fullpath);
     } catch(e) {
       return log.error(e.message);
     }
@@ -68,13 +54,13 @@ Plugin.prototype.delete = function() {
 };
 
 Plugin.prototype.save = function() {
-  const data = cache.get(h.KEYS.plugins) || {};
+  const stats = cache.get(h.KEYS.plugins) || {};
 
-  if (this.deleted) delete data[this.name];
+  if (this.deleted) delete stats[this.name];
   else if (this.missing) return;
-  else data[this.name] = this.enabled;
+  else stats[this.name] = this.enabled;
 
-  cache.set(h.KEYS.plugins, data);
+  cache.set(h.KEYS.plugins, stats);
 };
 
 Plugin.prototype.install = function(cb) {
@@ -83,9 +69,9 @@ Plugin.prototype.install = function(cb) {
   const cmd = 'npm install --save ' + this.deps.join(' ');
   log.debug(cmd);
   const spin = h.spin(cmd);
-  cp.exec(cmd, {cwd: h.getCodeDir()}, function() {
+  cp.exec(cmd, {cwd: file.codeDir()}, function(e) {
     spin.stop();
-    return cb();
+    return cb(e);
   });
 };
 
@@ -97,47 +83,62 @@ Plugin.init = function(head) {
   log.trace('initializing all plugins');
   head = head || require('./core');
 
-  // 1. check installed plugins
-  let plugins = [];
-  for (let f of h.getCodeDirData('lib/plugins')) {
+  const stats = cache.get(h.KEYS.plugins) || {};
+
+  // 1. find installed plugins
+  let installed = [];
+  for (let f of file.listCodeDir('lib/plugins')) {
     const p = f.data;
     if (!p) continue;
-
-    p.setFile(f.file);
     log.trace('found plugin: ' + p.name + '=' + p.ver);
+
+    p.file = f.file;
+    p.enabled = stats[p.name];
+
+    if (!(p.name in stats)) {
+      if (p.builtin) {
+        log.trace('new builtin plugin, enable by default');
+        p.enabled = true;
+      } else {
+        log.trace('new 3rd party plugin, disable by default');
+        p.enabled = false;
+      }
+    }
+    installed.push(p);
+  }
+  // the one with bigger `id` comes first
+  installed = _.sortBy(installed, x => -x.id);
+
+  // 2. init all in reversed order
+  for (let i = installed.length - 1; i >= 0; --i) {
+    const p = installed[i];
     if (p.enabled) {
       p.init();
       log.trace('inited plugin: ' + p.name);
     } else {
       log.trace('skipped plugin: ' + p.name);
     }
-
-    plugins.push(p);
   }
 
-  // chain the plugins together
-  // the one has bigger `id` comes first
-  plugins = _.sortBy(plugins, x => -x.id);
-
+  // 3. chain together
+  const plugins = installed.filter(x => x.enabled);
   let last = head;
   for (let p of plugins) {
-    if (!p.enabled) continue;
     last.setNext(p);
     last = p;
   }
 
-  // 2. check saved plugins
+  // 4. check missing plugins
   const missings = [];
-  const data = cache.get(h.KEYS.plugins) || {};
-  for (let k of _.keys(data)) {
-    if (plugins.find(x => x.name === k)) continue;
+  for (let k of _.keys(stats)) {
+    if (installed.find(x => x.name === k)) continue;
     const p = new Plugin(-1, k, 'missing');
-    p.enabled = data[k];
+    p.enabled = stats[k];
     missings.push(p);
+    log.trace('missing plugin:' + p.name);
   }
-  log.trace('missing plugins: ' + missings.length);
 
-  Plugin.plugins = plugins.concat(missings);
+  Plugin.plugins = installed.concat(missings);
   return missings.length === 0;
 };
 
@@ -146,23 +147,28 @@ Plugin.copy = function(src, cb) {
   if (path.extname(src) !== '.js') {
     src = config.sys.urls.plugin.replace('$name', src);
   }
-  const dst = h.getPluginFile(src);
+  const dst = file.pluginFile(src);
 
   const srcstream = src.startsWith('https://') ? request(src) : fs.createReadStream(src);
+  const dststream = fs.createWriteStream(dst);
+  let error;
+
   srcstream.on('response', function(resp) {
     if (resp.statusCode !== 200)
       srcstream.emit('error', 'HTTP Error: ' + resp.statusCode);
   });
   srcstream.on('error', function(e) {
-    spin.stop();
-    fs.unlinkSync(dst);
-    return cb(e);
+    dststream.emit('error', e);
   });
 
-  const dststream = fs.createWriteStream(dst);
+  dststream.on('error', function(e) {
+    error = e;
+    dststream.end();
+  });
   dststream.on('close', function() {
     spin.stop();
-    return cb(null, dst);
+    if (error) file.rm(dst);
+    return cb(error, dst);
   });
 
   log.debug('copying from ' + src);
@@ -176,6 +182,7 @@ Plugin.install = function(name, cb) {
     log.debug('copied to ' + fullpath);
 
     const p = require(fullpath);
+    p.file = path.basename(fullpath);
     p.install(function() {
       return cb(null, p);
     });
@@ -186,7 +193,7 @@ Plugin.installMissings = function(cb) {
   function doTask(plugin, queue, cb) {
     Plugin.install(plugin.name, function(e, p) {
       if (!e) {
-        p.enable(plugin.enabled);
+        p.enabled = plugin.enabled;
         p.save();
         p.help();
       }
@@ -194,11 +201,11 @@ Plugin.installMissings = function(cb) {
     });
   }
 
-  const plugins = Plugin.plugins.filter(x => x.missing);
-  if (plugins.length === 0) return cb();
+  const missings = Plugin.plugins.filter(x => x.missing);
+  if (missings.length === 0) return cb();
 
   log.warn('Installing missing plugins, might take a while ...');
-  const q = new Queue(plugins, {}, doTask);
+  const q = new Queue(missings, {}, doTask);
   q.run(1, cb);
 };
 
diff --git a/lib/plugins/cache.js b/lib/plugins/cache.js
index efe8d231..677c6c84 100644
--- a/lib/plugins/cache.js
+++ b/lib/plugins/cache.js
@@ -9,11 +9,6 @@ var session = require('../session');
 
 const plugin = new Plugin(50, 'cache', '', 'Plugin to provide local cache.');
 
-plugin.init = function() {
-  Plugin.prototype.init.call(this);
-  cache.init();
-};
-
 plugin.getProblems = function(cb) {
   const problems = cache.get(h.KEYS.problems);
   if (problems) {
diff --git a/lib/plugins/leetcode.js b/lib/plugins/leetcode.js
index 186e00fd..a2e17b8f 100644
--- a/lib/plugins/leetcode.js
+++ b/lib/plugins/leetcode.js
@@ -8,6 +8,7 @@ var request = require('request');
 
 var config = require('../config');
 var h = require('../helper');
+var file = require('../file');
 var log = require('../log');
 var Plugin = require('../plugin');
 var Queue = require('../queue');
@@ -19,24 +20,24 @@ const plugin = new Plugin(10, 'leetcode', '',
 var spin;
 
 // update options with user credentials
-function signOpts(opts, user) {
+plugin.signOpts = function(opts, user) {
   opts.headers.Cookie = 'LEETCODE_SESSION=' + user.sessionId +
                         ';csrftoken=' + user.sessionCSRF + ';';
   opts.headers['X-CSRFToken'] = user.sessionCSRF;
   opts.headers['X-Requested-With'] = 'XMLHttpRequest';
-}
+};
 
-function makeOpts(url) {
+plugin.makeOpts = function(url) {
   const opts = {};
   opts.url = url;
   opts.headers = {};
 
   if (session.isLogin())
-    signOpts(opts, session.getUser());
+    plugin.signOpts(opts, session.getUser());
   return opts;
-}
+};
 
-function checkError(e, resp, expectedStatus) {
+plugin.checkError = function(e, resp, expectedStatus) {
   if (!e && resp && resp.statusCode !== expectedStatus) {
     const code = resp.statusCode;
     log.debug('http error: ' + code);
@@ -48,6 +49,10 @@ function checkError(e, resp, expectedStatus) {
     }
   }
   return e;
+};
+
+plugin.init = function() {
+  config.app = 'leetcode';
 }
 
 plugin.getProblems = function(cb) {
@@ -75,11 +80,11 @@ plugin.getProblems = function(cb) {
 
 plugin.getCategoryProblems = function(category, cb) {
   log.debug('running leetcode.getCategoryProblems: ' + category);
-  const opts = makeOpts(config.sys.urls.problems.replace('$category', category));
+  const opts = plugin.makeOpts(config.sys.urls.problems.replace('$category', category));
 
   spin.text = 'Downloading category ' + category;
   request(opts, function(e, resp, body) {
-    e = checkError(e, resp, 200);
+    e = plugin.checkError(e, resp, 200);
     if (e) return cb(e);
 
     const json = JSON.parse(body);
@@ -118,7 +123,7 @@ plugin.getProblem = function(problem, cb) {
   const user = session.getUser();
   if (problem.locked && !user.paid) return cb('failed to load locked problem!');
 
-  const opts = makeOpts(config.sys.urls.graphql);
+  const opts = plugin.makeOpts(config.sys.urls.graphql);
   opts.headers.Origin = config.sys.urls.base;
   opts.headers.Referer = problem.link;
 
@@ -144,7 +149,7 @@ plugin.getProblem = function(problem, cb) {
   const spin = h.spin('Downloading ' + problem.slug);
   request.post(opts, function(e, resp, body) {
     spin.stop();
-    e = checkError(e, resp, 200);
+    e = plugin.checkError(e, resp, 200);
     if (e) return cb(e);
 
     const q = body.data.question;
@@ -177,16 +182,16 @@ function runCode(opts, problem, cb) {
 
   opts.body = opts.body || {};
   _.extendOwn(opts.body, {
-    lang:        h.extToLang(problem.file),
+    lang:        problem.lang,
     question_id: parseInt(problem.id, 10),
     test_mode:   false,
-    typed_code:  h.getFileData(problem.file)
+    typed_code:  file.data(problem.file)
   });
 
   const spin = h.spin('Sending code to judge');
   request(opts, function(e, resp, body) {
     spin.stop();
-    e = checkError(e, resp, 200);
+    e = plugin.checkError(e, resp, 200);
     if (e) return cb(e);
 
     if (body.error) {
@@ -219,7 +224,7 @@ function verifyResult(task, queue, cb) {
   const spin = h.spin('Waiting for judge result');
   request(opts, function(e, resp, body) {
     spin.stop();
-    e = checkError(e, resp, 200);
+    e = plugin.checkError(e, resp, 200);
     if (e) return cb(e);
 
     let result = JSON.parse(body);
@@ -268,7 +273,7 @@ function formatResult(result) {
 
 plugin.testProblem = function(problem, cb) {
   log.debug('running leetcode.testProblem');
-  const opts = makeOpts(config.sys.urls.test.replace('$slug', problem.slug));
+  const opts = plugin.makeOpts(config.sys.urls.test.replace('$slug', problem.slug));
   opts.body = {data_input: problem.testcase};
 
   runCode(opts, problem, function(e, task) {
@@ -287,7 +292,7 @@ plugin.testProblem = function(problem, cb) {
 
 plugin.submitProblem = function(problem, cb) {
   log.debug('running leetcode.submitProblem');
-  const opts = makeOpts(config.sys.urls.submit.replace('$slug', problem.slug));
+  const opts = plugin.makeOpts(config.sys.urls.submit.replace('$slug', problem.slug));
   opts.body = {judge_type: 'large'};
 
   runCode(opts, problem, function(e, task) {
@@ -303,11 +308,11 @@ plugin.submitProblem = function(problem, cb) {
 
 plugin.getSubmissions = function(problem, cb) {
   log.debug('running leetcode.getSubmissions');
-  const opts = makeOpts(config.sys.urls.submissions.replace('$slug', problem.slug));
+  const opts = plugin.makeOpts(config.sys.urls.submissions.replace('$slug', problem.slug));
   opts.headers.Referer = config.sys.urls.problem.replace('$slug', problem.slug);
 
   request(opts, function(e, resp, body) {
-    e = checkError(e, resp, 200);
+    e = plugin.checkError(e, resp, 200);
     if (e) return cb(e);
 
     // FIXME: this only return the 1st 20 submissions, we should get next if necessary.
@@ -321,10 +326,10 @@ plugin.getSubmissions = function(problem, cb) {
 
 plugin.getSubmission = function(submission, cb) {
   log.debug('running leetcode.getSubmission');
-  const opts = makeOpts(config.sys.urls.submission.replace('$id', submission.id));
+  const opts = plugin.makeOpts(config.sys.urls.submission.replace('$id', submission.id));
 
   request(opts, function(e, resp, body) {
-    e = checkError(e, resp, 200);
+    e = plugin.checkError(e, resp, 200);
     if (e) return cb(e);
 
     let re = body.match(/submissionCode:\s('[^']*')/);
@@ -338,7 +343,7 @@ plugin.getSubmission = function(submission, cb) {
 
 plugin.starProblem = function(problem, starred, cb) {
   log.debug('running leetcode.starProblem');
-  const opts = makeOpts();
+  const opts = plugin.makeOpts();
   opts.headers.Origin = config.sys.urls.base;
   opts.headers.Referer = problem.link;
 
@@ -359,7 +364,7 @@ plugin.starProblem = function(problem, starred, cb) {
   }
 
   request(opts, function(e, resp, body) {
-    e = checkError(e, resp, 204);
+    e = plugin.checkError(e, resp, 204);
     if (e) return cb(e);
 
     cb(null, starred);
@@ -368,12 +373,12 @@ plugin.starProblem = function(problem, starred, cb) {
 
 plugin.getFavorites = function(cb) {
   log.debug('running leetcode.getFavorites');
-  const opts = makeOpts(config.sys.urls.favorites);
+  const opts = plugin.makeOpts(config.sys.urls.favorites);
 
   const spin = h.spin('Retrieving user favorites');
   request(opts, function(e, resp, body) {
     spin.stop();
-    e = checkError(e, resp, 200);
+    e = plugin.checkError(e, resp, 200);
     if (e) return cb(e);
 
     const favorites = JSON.parse(body);
@@ -383,7 +388,7 @@ plugin.getFavorites = function(cb) {
 
 plugin.getUserInfo = function(cb) {
   log.debug('running leetcode.getUserInfo');
-  const opts = makeOpts(config.sys.urls.graphql);
+  const opts = plugin.makeOpts(config.sys.urls.graphql);
   opts.headers.Origin = config.sys.urls.base;
   opts.headers.Referer = config.sys.urls.base;
   opts.json = true;
@@ -402,7 +407,7 @@ plugin.getUserInfo = function(cb) {
   const spin = h.spin('Retrieving user profile');
   request.post(opts, function(e, resp, body) {
     spin.stop();
-    e = checkError(e, resp, 200);
+    e = plugin.checkError(e, resp, 200);
     if (e) return cb(e);
 
     const user = body.data.user;
@@ -411,7 +416,7 @@ plugin.getUserInfo = function(cb) {
 };
 
 function runSession(method, data, cb) {
-  const opts = makeOpts(config.sys.urls.session);
+  const opts = plugin.makeOpts(config.sys.urls.session);
   opts.json = true;
   opts.method = method;
   opts.body = data;
@@ -419,7 +424,7 @@ function runSession(method, data, cb) {
   const spin = h.spin('Waiting session result');
   request(opts, function(e, resp, body) {
     spin.stop();
-    e = checkError(e, resp, 200);
+    e = plugin.checkError(e, resp, 200);
     if (e && e.statusCode === 302) e = session.errors.EXPIRED;
 
     return e ? cb(e) : cb(null, body.sessions);
@@ -454,7 +459,7 @@ plugin.signin = function(user, cb) {
   const spin = h.spin('Signing in leetcode.com');
   request(config.sys.urls.login, function(e, resp, body) {
     spin.stop();
-    e = checkError(e, resp, 200);
+    e = plugin.checkError(e, resp, 200);
     if (e) return cb(e);
 
     user.loginCSRF = h.getSetCookieValue(resp, 'csrftoken');
diff --git a/lib/sprintf.js b/lib/sprintf.js
new file mode 100644
index 00000000..739f3d06
--- /dev/null
+++ b/lib/sprintf.js
@@ -0,0 +1,60 @@
+'use strict'
+
+function len(s) {
+  let s1 = s.replace(/\u001b\[[^m]*m/g, ''); // remove color controls
+  s1 = s1.replace(/[^\x00-\xff]/g, '  '); // fix non-ascii
+  return s1.length;
+}
+
+function padLeft(s, n, c) {
+  let k = Math.max(0, n - len(s));
+  return c.repeat(k) + s;
+}
+
+function padRight(s, n , c) {
+  let k = Math.max(0, n - len(s));
+  return s + c.repeat(k);
+}
+
+function padCenter(s, n, c) {
+  let k = Math.max(0, n - len(s));
+  let r = (k - k % 2) / 2, l = k - r;
+  return c.repeat(l) + s + c.repeat(r);
+}
+
+const tsprintf = function() {
+  const args = Array.from(arguments);
+  let fmt = args.shift();
+  return fmt.replace(/%[^s%]*[s%]/g, function(s) {
+    if (s === '%%') return '%';
+
+    let x = '' + args.shift();
+    let n = 0;
+
+    s = s.slice(1, s.length-1);
+    if (s.length > 0) {
+      switch (s[0]) {
+        case '-':
+          n = parseInt(s.slice(1)) || 0;
+          x = padRight(x, n, ' ');
+          break;
+        case '=':
+          n = parseInt(s.slice(1)) || 0;
+          x = padCenter(x, n, ' ');
+          break;
+        case '0':
+          n = parseInt(s.slice(1)) || 0;
+          x = padLeft(x, n, '0');
+          break;
+        default:
+          n = parseInt(s) || 0;
+          x = padLeft(x, n, ' ');
+          break;
+      }
+    }
+
+    return x;
+  });
+};
+
+module.exports = tsprintf;
diff --git a/package.json b/package.json
index d61ef84a..ee8f2414 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "leetcode-cli",
-  "version": "2.5.2",
+  "version": "2.6.1",
   "description": "A cli tool to enjoy leetcode!",
   "preferGlobal": "true",
   "engines": {
@@ -11,7 +11,24 @@
   },
   "scripts": {
     "lint": "eslint lib/ test/",
-    "test": "npm run lint && nyc mocha test/** && nyc report --reporter=lcov"
+    "test": "npm run lint && nyc mocha test test/plugins && nyc report --reporter=lcov",
+    "travis": "node bin/pkg",
+    "pkg": "pkg . --out-path=dist/ --targets"
+  },
+  "pkg": {
+    "scripts": [
+      "lib"
+    ],
+    "assets": [
+      "colors",
+      "icons",
+      "templates"
+    ],
+    "targets": [
+      "node10-linux-x64",
+      "node10-macos-x64",
+      "node10-win-x64"
+    ]
   },
   "repository": {
     "type": "git",
@@ -33,28 +50,28 @@
   },
   "homepage": "https://github.com/skygragon/leetcode-cli#readme",
   "dependencies": {
-    "ansi-styles": "3.2.0",
+    "ansi-styles": "3.2.1",
     "cheerio": "0.20.0",
-    "he": "1.1.1",
+    "he": "1.2.0",
     "mkdirp": "0.5.1",
     "moment": "^2.20.1",
     "nconf": "0.10.0",
-    "ora": "^1.3.0",
+    "ora": "3.0.0",
     "prompt": "1.0.0",
-    "request": "2.83.0",
-    "sprintf-js": "1.1.1",
-    "supports-color": "5.1.0",
-    "underscore": "1.8.3",
+    "request": "2.88.0",
+    "supports-color": "5.5.0",
+    "underscore": "1.9.1",
     "wordwrap": "1.0.0",
-    "yargs": "10.0.3"
+    "yargs": "12.0.4"
   },
   "devDependencies": {
-    "chai": "4.1.2",
-    "eslint": "4.13.1",
-    "eslint-config-google": "0.9.1",
-    "mocha": "4.0.1",
-    "nock": "9.1.4",
-    "nyc": "11.3.0",
-    "rewire": "2.5.2"
+    "chai": "4.2.0",
+    "eslint": "5.9.0",
+    "eslint-config-google": "0.11.0",
+    "mocha": "5.2.0",
+    "nock": "10.0.2",
+    "nyc": "13.1.0",
+    "pkg": "^4.3.4",
+    "rewire": "4.0.1"
   }
 }
diff --git a/templates/codeonly.tpl b/templates/codeonly.tpl
index 38efb755..d8baa802 100644
--- a/templates/codeonly.tpl
+++ b/templates/codeonly.tpl
@@ -1 +1 @@
-<%= code %>
+${code}
diff --git a/templates/detailed.tpl b/templates/detailed.tpl
index b8a762e2..cf501c0d 100644
--- a/templates/detailed.tpl
+++ b/templates/detailed.tpl
@@ -1,14 +1,16 @@
-<%= comment.start %>
-<%= comment.line %> [<%= id %>] <%= name %>
-<%= comment.line %>
-<%= comment.line %> <%= link %>
-<%= comment.line %>
-<%= comment.line %> <%= category %>
-<%= comment.line %> <%= level %> (<%= percent %>%)
-<%= comment.line %> Total Accepted:    <%= totalAC %>
-<%= comment.line %> Total Submissions: <%= totalSubmit %>
-<%= comment.line %> Testcase Example:  <%= testcase %>
-<%= comment.line %>
-<% desc.forEach(function(x) { %><%= comment.line %> <%= x %>
-<% }) %><%= comment.end %>
-<%= code %>
+${comment.start}
+${comment.line} @lc app=${app} id=${fid} lang=${lang}
+${comment.line}
+${comment.line} [${fid}] ${name}
+${comment.line}
+${comment.line} ${link}
+${comment.line}
+${comment.line} ${category}
+${comment.line} ${level} (${percent}%)
+${comment.line} Total Accepted:    ${totalAC}
+${comment.line} Total Submissions: ${totalSubmit}
+${comment.line} Testcase Example:  ${testcase}
+${comment.line}
+{{ desc.forEach(function(x) { }}${comment.line} ${x}
+{{ }) }}${comment.end}
+${code}
diff --git a/test/helper.js b/test/helper.js
index 56ee5363..ca0f532e 100644
--- a/test/helper.js
+++ b/test/helper.js
@@ -8,8 +8,13 @@ const h = {
 h.clean = function() {
   if (!fs.existsSync(this.DIR))
     fs.mkdirSync(this.DIR);
-  for (let f of fs.readdirSync(this.DIR))
-    fs.unlinkSync(this.DIR + f);
+  for (let f of fs.readdirSync(this.DIR)) {
+    const fullpath = this.DIR + f;
+    if (fs.statSync(fullpath).isDirectory())
+      fs.rmdirSync(fullpath);
+    else
+      fs.unlinkSync(fullpath);
+  }
 };
 
 module.exports = h;
diff --git a/test/plugins/test_cache.js b/test/plugins/test_cache.js
index a3980418..6b3114ca 100644
--- a/test/plugins/test_cache.js
+++ b/test/plugins/test_cache.js
@@ -3,6 +3,7 @@ const _ = require('underscore');
 const assert = require('chai').assert;
 const rewire = require('rewire');
 
+const h = require('../../lib/helper');
 const log = require('../../lib/log');
 const config = require('../../lib/config');
 const th = require('../helper');
@@ -11,7 +12,7 @@ describe('plugin:cache', function() {
   let plugin;
   let next;
   let cache;
-  let h;
+  let file;
   let session;
 
   const PROBLEMS = [
@@ -29,11 +30,11 @@ describe('plugin:cache', function() {
     th.clean();
     next = {};
 
-    h = rewire('../../lib/helper');
-    h.getCacheDir = () => th.DIR;
+    file = rewire('../../lib/file');
+    file.cacheDir = () => th.DIR;
 
     cache = rewire('../../lib/cache');
-    cache.__set__('h', h);
+    cache.__set__('file', file);
     cache.init();
 
     session = rewire('../../lib/session');
diff --git a/test/test_cache.js b/test/test_cache.js
index b3fb669d..caba14c1 100644
--- a/test/test_cache.js
+++ b/test/test_cache.js
@@ -13,11 +13,11 @@ describe('cache', function() {
   beforeEach(function() {
     th.clean();
 
-    const h = rewire('../lib/helper');
-    h.getCacheDir = () => th.DIR;
+    const file = rewire('../lib/file');
+    file.cacheDir = () => th.DIR;
 
     cache = rewire('../lib/cache');
-    cache.__set__('h', h);
+    cache.__set__('file', file);
     cache.init();
   });
 
diff --git a/test/test_config.js b/test/test_config.js
index 6cfc0210..9ae828dd 100644
--- a/test/test_config.js
+++ b/test/test_config.js
@@ -12,11 +12,11 @@ describe('config', function() {
   beforeEach(function() {
     th.clean();
 
-    const h = rewire('../lib/helper');
-    h.getConfigFile = () => FILE;
+    const file = rewire('../lib/file');
+    file.configFile = () => FILE;
 
     config = rewire('../lib/config');
-    config.__set__('h', h);
+    config.__set__('file', file);
   });
 
   function createConfigFile(data) {
diff --git a/test/test_core.js b/test/test_core.js
index c61031c0..0a436bb4 100644
--- a/test/test_core.js
+++ b/test/test_core.js
@@ -142,7 +142,17 @@ describe('core', function() {
   }); // #starProblem
 
   describe('#exportProblem', function() {
+    let file;
+
+    beforeEach(function() {
+      file = rewire('../lib/file');
+      file.init();
+      core.__set__('file', file);
+    });
+
     it('should codeonly ok', function() {
+      file.isWindows = () => false;
+
       const expected = [
         '/**',
         ' * Definition for singly-linked list.',
@@ -171,9 +181,7 @@ describe('core', function() {
     });
 
     it('should codeonly ok in windows', function() {
-      const h = rewire('../lib/helper');
-      h.isWindows = () => true;
-      core.__set__('h', h);
+      file.isWindows = () => true;
 
       const expected = [
         '/**',
@@ -203,8 +211,12 @@ describe('core', function() {
     });
 
     it('should detailed ok with cpp', function() {
+      file.isWindows = () => false;
+
       const expected = [
         '/*',
+        ' * @lc app=leetcode id=2 lang=cpp',
+        ' *',
         ' * [2] Add Two Numbers',
         ' *',
         ' * https://leetcode.com/problems/add-two-numbers',
@@ -249,7 +261,11 @@ describe('core', function() {
     });
 
     it('should detailed ok with ruby', function() {
+      file.isWindows = () => false;
+
       const expected = [
+        '#',
+        '# @lc app=leetcode id=2 lang=ruby',
         '#',
         '# [2] Add Two Numbers',
         '#',
diff --git a/test/test_file.js b/test/test_file.js
new file mode 100644
index 00000000..d458e83f
--- /dev/null
+++ b/test/test_file.js
@@ -0,0 +1,163 @@
+'use strict';
+const fs = require('fs');
+const path = require('path');
+
+const assert = require('chai').assert;
+const rewire = require('rewire');
+
+const th = require('./helper');
+
+describe('file', function() {
+  let file;
+
+  beforeEach(function() {
+    file = rewire('../lib/file');
+  });
+
+  describe('#dirAndFiles', function() {
+    const HOME = path.join(__dirname, '..');
+
+    it('should ok on linux', function() {
+      if (file.isWindows()) this.skip();
+      process.env.HOME = '/home/skygragon';
+
+      assert.equal(file.userHomeDir(), '/home/skygragon');
+      assert.equal(file.homeDir(), '/home/skygragon/.lc');
+      assert.equal(file.cacheDir(), '/home/skygragon/.lc/leetcode/cache');
+      assert.equal(file.cacheFile('xxx'), '/home/skygragon/.lc/leetcode/cache/xxx.json');
+      assert.equal(file.configFile(), '/home/skygragon/.lc/config.json');
+      assert.equal(file.name('/home/skygragon/.lc/leetcode/cache/xxx.json'), 'xxx');
+    });
+
+    it('should ok on windows', function() {
+      if (!file.isWindows()) this.skip();
+      process.env.HOME = '';
+      process.env.USERPROFILE = 'C:\\Users\\skygragon';
+      assert.equal(file.userHomeDir(), 'C:\\Users\\skygragon');
+      assert.equal(file.homeDir(), 'C:\\Users\\skygragon\\.lc');
+      assert.equal(file.cacheDir(), 'C:\\Users\\skygragon\\.lc\\leetcode\\cache');
+      assert.equal(file.cacheFile('xxx'), 'C:\\Users\\skygragon\\.lc\\leetcode\\cache\\xxx.json');
+      assert.equal(file.configFile(), 'C:\\Users\\skygragon\\.lc\\config.json');
+      assert.equal(file.name('C:\\Users\\skygragon\\.lc\\leetcode\\cache\\xxx.json'), 'xxx');
+    });
+
+    it('should codeDir ok', function() {
+      assert.equal(file.codeDir(), HOME);
+      assert.equal(file.codeDir('.'), HOME);
+      assert.equal(file.codeDir('icons'), path.join(HOME, 'icons'));
+      assert.equal(file.codeDir('lib/plugins'), path.join(HOME, 'lib', 'plugins'));
+    });
+
+    it('should listCodeDir ok', function() {
+      const files = file.listCodeDir('lib/plugins');
+      assert.equal(files.length, 3);
+      assert.equal(files[0].name, 'cache');
+      assert.equal(files[1].name, 'leetcode');
+      assert.equal(files[2].name, 'retry');
+    });
+
+    it('should pluginFile ok', function() {
+      const expect = path.join(HOME, 'lib/plugins/cache.js');
+      assert.equal(file.pluginFile('cache.js'), expect);
+      assert.equal(file.pluginFile('./cache.js'), expect);
+      assert.equal(file.pluginFile('https://github.com/skygragon/cache.js'), expect);
+    });
+
+    it('should data ok with missing file', function() {
+      assert.equal(file.data('non-exist'), null);
+    });
+  }); // #dirAndFiles
+
+  describe('#meta', function() {
+    it('should meta ok within file content', function() {
+      file.data = x => [
+        '/ *',
+        '  * @lc app=leetcode id=123 lang=javascript',
+        '  * /'
+      ].join('\n');
+      const meta = file.meta('dummy');
+      assert.equal(meta.app, 'leetcode')
+      assert.equal(meta.id, '123');
+      assert.equal(meta.lang, 'javascript');
+    });
+
+    it('should meta ok with white space', function() {
+      file.data = x => [
+        '/ *',
+        '  * @lc app=leetcode id=123\t  \t  lang=javascript\r',
+        '  * /'
+      ].join('\n');
+      const meta = file.meta('dummy');
+      assert.equal(meta.app, 'leetcode')
+      assert.equal(meta.id, '123');
+      assert.equal(meta.lang, 'javascript');
+    });
+
+    it('should meta ok within file name', function() {
+      file.data = x => [
+        '/ *',
+        '  * no meta app=leetcode id=123 lang=javascript',
+        '  * /'
+      ].join('\n');
+      const meta = file.meta('321.dummy.py');
+      assert(!meta.app)
+      assert.equal(meta.id, '321');
+      assert.equal(meta.lang, 'python');
+    });
+
+    it('should meta ok within deprecated file name', function() {
+      file.data = x => [
+        '/ *',
+        '  * no meta app=leetcode id=123 lang=javascript',
+        '  * /'
+      ].join('\n');
+
+      var meta = file.meta('111.dummy.py3');
+      assert(!meta.app)
+      assert.equal(meta.id, '111');
+      assert.equal(meta.lang, 'python3');
+
+      meta = file.meta('222.dummy.python3.py');
+      assert(!meta.app)
+      assert.equal(meta.id, '222');
+      assert.equal(meta.lang, 'python3');
+    });
+
+    it('should fmt ok', function() {
+      file.init();
+      const data = file.fmt('${id}', {id: 123});
+      assert.equal(data, '123');
+    });
+  }); // #meta
+
+  describe('#genneral', function() {
+    beforeEach(function() {
+      th.clean();
+    });
+    afterEach(function() {
+      th.clean();
+    });
+
+    it('should mkdir ok', function() {
+      const dir = th.DIR + 'dir';
+      assert.equal(fs.existsSync(dir), false);
+      file.mkdir(dir);
+      assert.equal(fs.existsSync(dir), true);
+      file.mkdir(dir);
+      assert.equal(fs.existsSync(dir), true);
+    });
+
+    it('should mv ok', function() {
+      const SRC = th.Dir + 'src';
+      const DST = th.DIR + 'dst';
+      assert.equal(fs.existsSync(SRC), false);
+      assert.equal(fs.existsSync(DST), false);
+      file.mkdir(SRC);
+      assert.equal(fs.existsSync(SRC), true);
+      assert.equal(fs.existsSync(DST), false);
+      file.mv(SRC, DST);
+      assert.equal(fs.existsSync(SRC), false);
+      assert.equal(fs.existsSync(DST), true);
+    });
+  }); // #general
+});
diff --git a/test/test_helper.js b/test/test_helper.js
index ac47664e..cfc502d4 100644
--- a/test/test_helper.js
+++ b/test/test_helper.js
@@ -1,6 +1,4 @@
 'use strict';
-const path = require('path');
-
 const assert = require('chai').assert;
 const rewire = require('rewire');
 const _ = require('underscore');
@@ -131,7 +129,7 @@ describe('helper', function() {
       assert.equal(h.langToExt('javascript'), '.js');
       assert.equal(h.langToExt('mysql'), '.sql');
       assert.equal(h.langToExt('python'), '.py');
-      assert.equal(h.langToExt('python3'), '.python3.py');
+      assert.equal(h.langToExt('python3'), '.py');
       assert.equal(h.langToExt('ruby'), '.rb');
       assert.equal(h.langToExt('scala'), '.scala');
       assert.equal(h.langToExt('swift'), '.swift');
@@ -149,8 +147,6 @@ describe('helper', function() {
       assert.equal(h.extToLang('file.java'), 'java');
       assert.equal(h.extToLang('c:/file.js'), 'javascript');
       assert.equal(h.extToLang('c:/Users/skygragon/file.py'), 'python');
-      assert.equal(h.extToLang('c:/Users/skygragon/file.py3'), 'python3');
-      assert.equal(h.extToLang('c:/Users/skygragon/file.python3.py'), 'python3');
       assert.equal(h.extToLang('~/file.rb'), 'ruby');
       assert.equal(h.extToLang('/tmp/file.scala'), 'scala');
       assert.equal(h.extToLang('~/leetcode/file.swift'), 'swift');
@@ -163,6 +159,7 @@ describe('helper', function() {
     it('should ok', function() {
       const C_STYLE = {start: '/*', line: ' *', end: ' */'};
       const RUBY_STYLE = {start: '#', line: '#', end: '#'};
+      const SQL_STYLE = {start: '--', line: '--', end: '--'};
 
       assert.deepEqual(h.langToCommentStyle('bash'), RUBY_STYLE);
       assert.deepEqual(h.langToCommentStyle('c'), C_STYLE);
@@ -171,7 +168,7 @@ describe('helper', function() {
       assert.deepEqual(h.langToCommentStyle('golang'), C_STYLE);
       assert.deepEqual(h.langToCommentStyle('java'), C_STYLE);
       assert.deepEqual(h.langToCommentStyle('javascript'), C_STYLE);
-      assert.deepEqual(h.langToCommentStyle('mysql'), RUBY_STYLE);
+      assert.deepEqual(h.langToCommentStyle('mysql'), SQL_STYLE);
       assert.deepEqual(h.langToCommentStyle('python'), RUBY_STYLE);
       assert.deepEqual(h.langToCommentStyle('python3'), RUBY_STYLE);
       assert.deepEqual(h.langToCommentStyle('ruby'), RUBY_STYLE);
@@ -180,51 +177,6 @@ describe('helper', function() {
     });
   }); // #langToCommentStyle
 
-  describe('#dirAndFiles', function() {
-    const HOME = path.join(__dirname, '..');
-
-    it('should ok', function() {
-      process.env.HOME = '/home/skygragon';
-
-      assert.equal(h.getUserHomeDir(), '/home/skygragon');
-      assert.equal(h.getHomeDir(), '/home/skygragon/.lc');
-      assert.equal(h.getCacheDir(), '/home/skygragon/.lc/cache');
-      assert.equal(h.getCacheFile('xxx'), '/home/skygragon/.lc/cache/xxx.json');
-      assert.equal(h.getConfigFile(), '/home/skygragon/.lc/config.json');
-      assert.equal(h.getFilename('/home/skygragon/.lc/cache/xxx.json'), 'xxx');
-
-      process.env.HOME = '';
-      process.env.USERPROFILE = 'C:\\Users\\skygragon';
-      assert.equal(h.getUserHomeDir(), 'C:\\Users\\skygragon');
-    });
-
-    it('should getCodeDir ok', function() {
-      assert.equal(h.getCodeDir(), HOME);
-      assert.equal(h.getCodeDir('.'), HOME);
-      assert.equal(h.getCodeDir('icons'), path.join(HOME, 'icons'));
-      assert.equal(h.getCodeDir('lib/plugins'), path.join(HOME, 'lib', 'plugins'));
-    });
-
-    it('should getCodeDirData ok', function() {
-      const files = h.getCodeDirData('lib/plugins');
-      assert.equal(files.length, 3);
-      assert.equal(files[0].name, 'cache');
-      assert.equal(files[1].name, 'leetcode');
-      assert.equal(files[2].name, 'retry');
-    });
-
-    it('should getPluginFile ok', function() {
-      const expect = path.join(HOME, 'lib/plugins/cache.js');
-      assert.equal(h.getPluginFile('cache.js'), expect);
-      assert.equal(h.getPluginFile('./cache.js'), expect);
-      assert.equal(h.getPluginFile('https://github.com/skygragon/cache.js'), expect);
-    });
-
-    it('should getFileData ok with missing file', function() {
-      assert.equal(h.getFileData('non-exist'), null);
-    });
-  }); // #dirAndFiles
-
   describe('#getSetCookieValue', function() {
     it('should ok', function() {
       const resp = {
diff --git a/test/test_icon.js b/test/test_icon.js
index 8408a2c9..5da832ab 100644
--- a/test/test_icon.js
+++ b/test/test_icon.js
@@ -4,11 +4,11 @@ const rewire = require('rewire');
 
 describe('icon', function() {
   let icon;
-  let h;
+  let file;
 
   beforeEach(function() {
-    h = rewire('../lib/helper');
-    h.getCodeDirData = function() {
+    file = rewire('../lib/file');
+    file.listCodeDir = function() {
       return [
         {name: 'mac', data: {yes: 'yes', no: 'no', lock: 'lock', like: 'like', unlike: 'unlike'}},
         {name: 'win7', data: {yes: 'YES', no: 'NO', lock: 'LOCK', like: 'LIKE', unlike: 'UNLIKE'}}
@@ -16,7 +16,7 @@ describe('icon', function() {
     };
 
     icon = rewire('../lib/icon');
-    icon.__set__('h', h);
+    icon.__set__('file', file);
     icon.init();
   });
 
@@ -30,7 +30,9 @@ describe('icon', function() {
       assert.equal(icon.unlike, 'unlike');
     });
 
-    it('should ok with unknown theme', function() {
+    it('should ok with unknown theme on linux', function() {
+      file.isWindows = () => false;
+
       icon.setTheme('non-exist');
       assert.equal(icon.yes, '✔');
       assert.equal(icon.no, '✘');
@@ -40,7 +42,7 @@ describe('icon', function() {
     });
 
     it('should ok with unknown theme on windows', function() {
-      h.isWindows = () => true;
+      file.isWindows = () => true;
 
       icon.setTheme('non-exist');
       assert.equal(icon.yes, 'YES');
diff --git a/test/test_log.js b/test/test_log.js
index 3fd58ff6..92e5ef4b 100644
--- a/test/test_log.js
+++ b/test/test_log.js
@@ -93,16 +93,16 @@ describe('log', function() {
 
     it('should ok with log.fail', function() {
       log.fail({msg: 'some error', statusCode: 500});
-      assert.equal(expected, chalk.red('[ERROR] some error [500]'));
+      assert.equal(expected, chalk.red('[ERROR] some error [code=500]'));
 
       log.fail('some error');
-      assert.equal(expected, chalk.red('[ERROR] some error [0]'));
+      assert.equal(expected, chalk.red('[ERROR] some error'));
     });
   }); // #levels
 
   describe('#printf', function() {
     it('should ok', function() {
-      log.printf('%s and %d and %%', 'string', 100);
+      log.printf('%s and %s and %%', 'string', 100);
       assert.equal(expected, 'string and 100 and %');
     });
   }); // #printf
diff --git a/test/test_plugin.js b/test/test_plugin.js
index d758b9b9..aa3a40be 100644
--- a/test/test_plugin.js
+++ b/test/test_plugin.js
@@ -13,7 +13,7 @@ const th = require('./helper');
 const Plugin = rewire('../lib/plugin');
 
 describe('plugin', function() {
-  let h;
+  let file;
   let cache;
 
   const NOOP = () => {};
@@ -23,9 +23,9 @@ describe('plugin', function() {
     chalk.init();
     config.init();
 
-    h = rewire('../lib/helper');
+    file = rewire('../lib/file');
     cache = rewire('../lib/cache');
-    Plugin.__set__('h', h);
+    Plugin.__set__('file', file);
     Plugin.__set__('cache', cache);
   });
 
@@ -35,17 +35,17 @@ describe('plugin', function() {
   });
 
   describe('#Plugin.init', function() {
-    const p1 = new Plugin(0, 'Leetcode', '2.0');
-    const p2 = new Plugin(1, 'Cache', '1.0');
-    const p3 = new Plugin(2, 'Retry', '3.0');
-    const p4 = new Plugin(3, 'Core', '4.0');
+    const p1 = new Plugin(0, 'leetcode', '2.0');
+    const p2 = new Plugin(1, 'cache', '1.0');
+    const p3 = new Plugin(2, 'retry', '3.0');
+    const p4 = new Plugin(3, 'core', '4.0');
 
     before(function() {
       p1.init = p2.init = p3.init = p4.init = NOOP;
-      h.getCodeDirData = function() {
+      file.listCodeDir = function() {
         return [
           {name: 'cache', data: p2, file: 'cache.js'},
-          {name: 'leetcode', data: p1, file: '.leetcode.js'},  // disabled
+          {name: 'leetcode', data: p1, file: 'leetcode.js'},
           {name: 'retry', data: p3, file: 'retry.js'},
           {name: 'bad', data: null}
         ];
@@ -53,6 +53,9 @@ describe('plugin', function() {
     });
 
     it('should init ok', function() {
+      cache.get = () => {
+        return {cache: true, leetcode: false, retry: true};
+      };
       assert.deepEqual(Plugin.plugins, []);
 
       const res = Plugin.init(p4);
@@ -60,7 +63,7 @@ describe('plugin', function() {
       assert.deepEqual(Plugin.plugins.length, 3);
 
       const names = Plugin.plugins.map(p => p.name);
-      assert.deepEqual(names, ['Retry', 'Cache', 'Leetcode']);
+      assert.deepEqual(names, ['retry', 'cache', 'leetcode']);
 
       assert.equal(p4.next, p3);
       assert.equal(p3.next, p2);
@@ -70,7 +73,7 @@ describe('plugin', function() {
 
     it('should find missing ok', function() {
       cache.get = () => {
-        return {company: true, solution: true};
+        return {company: true, leetcode: false, solution: true};
       };
 
       const res = Plugin.init(p4);
@@ -78,7 +81,7 @@ describe('plugin', function() {
       assert.deepEqual(Plugin.plugins.length, 5);
 
       const names = Plugin.plugins.map(p => p.name);
-      assert.deepEqual(names, ['Retry', 'Cache', 'Leetcode', 'company', 'solution']);
+      assert.deepEqual(names, ['retry', 'cache', 'leetcode', 'company', 'solution']);
 
       assert.equal(p4.next, p3);
       assert.equal(p3.next, p2);
@@ -123,7 +126,7 @@ describe('plugin', function() {
     const DST = path.resolve(th.DIR, 'copy.test.js');
 
     before(function() {
-      h.getPluginFile = () => DST;
+      file.pluginFile = () => DST;
     });
 
     it('should copy from http error', function(done) {
@@ -143,9 +146,9 @@ describe('plugin', function() {
       ];
       fs.writeFileSync(SRC, data.join('\n'));
 
-      Plugin.install(SRC, function(e, p) {
+      Plugin.copy(SRC, function(e, fullpath) {
         assert.notExists(e);
-        assert.equal(p.x, 123);
+        assert.equal(fullpath, DST);
         assert.equal(fs.existsSync(DST), true);
         done();
       });
@@ -162,7 +165,7 @@ describe('plugin', function() {
 
     beforeEach(function() {
       expected = [];
-      h.getPluginFile = x => th.DIR + x;
+      file.pluginFile = x => th.DIR + x;
       Plugin.install = (name, cb) => {
         expected.push(name);
         return cb(null, PLUGINS[+name]);
@@ -179,36 +182,9 @@ describe('plugin', function() {
     });
   }); // #Plugin.installMissings
 
-  describe('#enable', function() {
-    const FILE = path.resolve(th.DIR, 'leetcode.js');
-
-    before(function() {
-      h.getPluginFile = () => FILE;
-    });
-
-    it('should ok', function() {
-      const p = new Plugin(0, 'Leetcode', '2.0', '');
-      assert.equal(p.enabled, true);
-
-      p.setFile('.leetcode.js');
-      fs.writeFileSync(FILE, '');
-      assert.equal(p.enabled, false);
-      assert.equal(p.file, '.leetcode.js');
-      p.enable(false);
-      assert.equal(p.enabled, false);
-      assert.equal(p.file, '.leetcode.js');
-      p.enable(true);
-      assert.equal(p.enabled, true);
-      assert.equal(p.file, 'leetcode.js');
-      p.enable(false);
-      assert.equal(p.enabled, false);
-      assert.equal(p.file, '.leetcode.js');
-    });
-  }); // #enable
-
   describe('#delete', function() {
     it('should ok', function() {
-      h.getPluginFile = x => th.DIR + x;
+      file.pluginFile = x => th.DIR + x;
 
       const p = new Plugin(0, '0', '2018.01.01');
       p.file = '0.js';
diff --git a/test/test_sprintf.js b/test/test_sprintf.js
new file mode 100644
index 00000000..aed7cbc3
--- /dev/null
+++ b/test/test_sprintf.js
@@ -0,0 +1,33 @@
+'use strict';
+const assert = require('chai').assert;
+const rewire = require('rewire');
+
+const sprintf = require('../lib/sprintf');
+
+describe('sprintf', function() {
+  it('should ok', function() {
+    assert.equal(sprintf('%%'), '%');
+    assert.equal(sprintf('%s', 123), '123');
+    assert.equal(sprintf('%6s', 123), '   123');
+    assert.equal(sprintf('%06s', 123), '000123');
+    assert.equal(sprintf('%-6s', 123), '123   ');
+    assert.equal(sprintf('%=6s', 123), '  123 ');
+
+    assert.equal(sprintf('%4s,%=4s,%-4s', 123, 'xy', 3.1), ' 123, xy ,3.1 ');
+  });
+
+  it('should non-ascii ok', function() {
+    assert.equal(sprintf('%4s', '中'), '  中');
+    assert.equal(sprintf('%-4s', '中'), '中  ');
+    assert.equal(sprintf('%=4s', '中'), ' 中 ');
+
+    assert.equal(sprintf('%=14s', '12你好34世界'), ' 12你好34世界 ');
+  });
+
+  it('should color ok', function() {
+    const chalk = rewire('../lib/chalk');
+    chalk.init();
+
+    assert.equal(sprintf('%=3s', chalk.red('X')), ' ' + chalk.red('X') + ' ');
+  });
+});
