revamped console ui (#56)

* revamped console ui
  - added spinner
  - more consistent logging & colors
  - highlight colors
  - central log.js

* await shutdown promise so that Debugger.run() returns only after shutdown
* update readme with --silent mode
diff --git a/README.md b/README.md
index ed01193..e9ad006 100644
--- a/README.md
+++ b/README.md
@@ -517,6 +517,7 @@
 
 Options:
   -v, --verbose  Verbose output. Logs activation parameters and result           [boolean]
+  -s, --silent   Silent. Only output logs from action container.                 [boolean]
   --version      Show version number                                             [boolean]
   -h, --help     Show help                                                       [boolean]
 ```
diff --git a/index.js b/index.js
index 5f267f7..108f72a 100644
--- a/index.js
+++ b/index.js
@@ -21,32 +21,7 @@
 const Debugger = require("./src/debugger");
 const path = require("path");
 const fs = require("fs");
-const debug = require('./src/debug');
-
-function enableConsoleColors() {
-    // colorful console.error() and co
-    let originalConsole = null;
-    if (!console._logToFile) {
-        originalConsole = {
-            log: console.log,
-            error: console.error,
-            info: console.info,
-            debug: console.debug
-        };
-        // overwrites console.log and co
-        require('manakin').global;
-    }
-    return originalConsole;
-}
-
-function resetConsoleColors(originalConsole) {
-    if (originalConsole) {
-        console.log = originalConsole.log;
-        console.error = originalConsole.error;
-        console.info = originalConsole.info;
-        console.debug = originalConsole.debug;
-    }
-}
+const log = require('./src/log');
 
 function getSupportedKinds() {
     const kinds = [];
@@ -216,6 +191,11 @@
         type: "boolean",
         describe: "Verbose output. Logs activation parameters and result"
     });
+    yargs.option("s", {
+        alias: "silent",
+        type: "boolean",
+        describe: "Silent. Only output logs from action container."
+    });
     yargs.version(require("./package.json").version);
 }
 
@@ -275,13 +255,9 @@
     }
 }
 
-function printErrorAndExit(err, argv) {
-    console.log();
-    if (argv.verbose) {
-        console.error(err);
-    } else {
-        console.error("Error:", err.message);
-    }
+function printErrorAndExit(err) {
+    log.log();
+    log.exception(err);
     process.exit(1);
 }
 
@@ -297,8 +273,8 @@
 }
 
 async function wskdebug(args, isCommandLine=false) {
-    debug("wskdebug arguments:", args);
-    const originalConsole = enableConsoleColors();
+    log.debug("wskdebug arguments:", args);
+    log.enableConsoleColors();
 
     try {
         const parser = getYargsParser();
@@ -315,6 +291,9 @@
             return;
         }
 
+        log.isVerbose = argv.verbose;
+        log.silent(argv.silent);
+
         try {
             const dbg = new Debugger(argv);
             if (isCommandLine) {
@@ -325,14 +304,14 @@
 
         } catch (e) {
             if (isCommandLine) {
-                printErrorAndExit(e, argv);
+                printErrorAndExit(e);
             } else {
                 throw e;
             }
         }
 
     } finally {
-        resetConsoleColors(originalConsole);
+        log.resetConsoleColors();
     }
 }
 
diff --git a/package-lock.json b/package-lock.json
index ce21bad..ae04f84 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -185,6 +185,52 @@
                 "@babel/helper-validator-identifier": "^7.9.0",
                 "chalk": "^2.0.0",
                 "js-tokens": "^4.0.0"
+            },
+            "dependencies": {
+                "ansi-styles": {
+                    "version": "3.2.1",
+                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+                    "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+                    "dev": true,
+                    "requires": {
+                        "color-convert": "^1.9.0"
+                    }
+                },
+                "chalk": {
+                    "version": "2.4.2",
+                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+                    "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+                    "dev": true,
+                    "requires": {
+                        "ansi-styles": "^3.2.1",
+                        "escape-string-regexp": "^1.0.5",
+                        "supports-color": "^5.3.0"
+                    }
+                },
+                "color-convert": {
+                    "version": "1.9.3",
+                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+                    "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+                    "dev": true,
+                    "requires": {
+                        "color-name": "1.1.3"
+                    }
+                },
+                "color-name": {
+                    "version": "1.1.3",
+                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+                    "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+                    "dev": true
+                },
+                "supports-color": {
+                    "version": "5.5.0",
+                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+                    "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+                    "dev": true,
+                    "requires": {
+                        "has-flag": "^3.0.0"
+                    }
+                }
             }
         },
         "@babel/parser": {
@@ -486,40 +532,12 @@
             "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
         },
         "chalk": {
-            "version": "2.4.2",
-            "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-            "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-            "dev": true,
+            "version": "4.0.0",
+            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz",
+            "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==",
             "requires": {
-                "ansi-styles": "^3.2.1",
-                "escape-string-regexp": "^1.0.5",
-                "supports-color": "^5.3.0"
-            },
-            "dependencies": {
-                "ansi-styles": {
-                    "version": "3.2.1",
-                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-                    "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-                    "dev": true,
-                    "requires": {
-                        "color-convert": "^1.9.0"
-                    }
-                },
-                "color-convert": {
-                    "version": "1.9.3",
-                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-                    "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-                    "dev": true,
-                    "requires": {
-                        "color-name": "1.1.3"
-                    }
-                },
-                "color-name": {
-                    "version": "1.1.3",
-                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-                    "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
-                    "dev": true
-                }
+                "ansi-styles": "^4.1.0",
+                "supports-color": "^7.1.0"
             }
         },
         "chardet": {
@@ -559,11 +577,15 @@
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
             "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
-            "dev": true,
             "requires": {
                 "restore-cursor": "^3.1.0"
             }
         },
+        "cli-spinners": {
+            "version": "2.3.0",
+            "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.3.0.tgz",
+            "integrity": "sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w=="
+        },
         "cli-width": {
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
@@ -681,6 +703,21 @@
                 "strip-bom": "^4.0.0"
             }
         },
+        "defaults": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
+            "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
+            "requires": {
+                "clone": "^1.0.2"
+            },
+            "dependencies": {
+                "clone": {
+                    "version": "1.0.4",
+                    "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+                    "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
+                }
+            }
+        },
         "define-properties": {
             "version": "1.1.3",
             "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@@ -762,8 +799,7 @@
         "escape-string-regexp": {
             "version": "1.0.5",
             "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-            "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
-            "dev": true
+            "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
         },
         "eslint": {
             "version": "6.8.0",
@@ -816,6 +852,41 @@
                     "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
                     "dev": true
                 },
+                "ansi-styles": {
+                    "version": "3.2.1",
+                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+                    "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+                    "dev": true,
+                    "requires": {
+                        "color-convert": "^1.9.0"
+                    }
+                },
+                "chalk": {
+                    "version": "2.4.2",
+                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+                    "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+                    "dev": true,
+                    "requires": {
+                        "ansi-styles": "^3.2.1",
+                        "escape-string-regexp": "^1.0.5",
+                        "supports-color": "^5.3.0"
+                    }
+                },
+                "color-convert": {
+                    "version": "1.9.3",
+                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+                    "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+                    "dev": true,
+                    "requires": {
+                        "color-name": "1.1.3"
+                    }
+                },
+                "color-name": {
+                    "version": "1.1.3",
+                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+                    "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+                    "dev": true
+                },
                 "debug": {
                     "version": "4.1.1",
                     "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -833,6 +904,15 @@
                     "requires": {
                         "ansi-regex": "^4.1.0"
                     }
+                },
+                "supports-color": {
+                    "version": "5.5.0",
+                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+                    "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+                    "dev": true,
+                    "requires": {
+                        "has-flag": "^3.0.0"
+                    }
                 }
             }
         },
@@ -1227,8 +1307,7 @@
         "has-flag": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-            "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
-            "dev": true
+            "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
         },
         "has-symbols": {
             "version": "1.0.1",
@@ -1468,6 +1547,11 @@
                 "is-extglob": "^2.1.1"
             }
         },
+        "is-interactive": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+            "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="
+        },
         "is-number": {
             "version": "7.0.0",
             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -1799,9 +1883,49 @@
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
             "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
-            "dev": true,
             "requires": {
                 "chalk": "^2.4.2"
+            },
+            "dependencies": {
+                "ansi-styles": {
+                    "version": "3.2.1",
+                    "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+                    "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+                    "requires": {
+                        "color-convert": "^1.9.0"
+                    }
+                },
+                "chalk": {
+                    "version": "2.4.2",
+                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+                    "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+                    "requires": {
+                        "ansi-styles": "^3.2.1",
+                        "escape-string-regexp": "^1.0.5",
+                        "supports-color": "^5.3.0"
+                    }
+                },
+                "color-convert": {
+                    "version": "1.9.3",
+                    "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+                    "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+                    "requires": {
+                        "color-name": "1.1.3"
+                    }
+                },
+                "color-name": {
+                    "version": "1.1.3",
+                    "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+                    "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+                },
+                "supports-color": {
+                    "version": "5.5.0",
+                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+                    "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+                    "requires": {
+                        "has-flag": "^3.0.0"
+                    }
+                }
             }
         },
         "make-dir": {
@@ -1821,8 +1945,7 @@
         "mimic-fn": {
             "version": "2.1.0",
             "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
-            "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
-            "dev": true
+            "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
         },
         "minimatch": {
             "version": "3.0.4",
@@ -2162,8 +2285,7 @@
         "mute-stream": {
             "version": "0.0.8",
             "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
-            "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
-            "dev": true
+            "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
         },
         "natural-compare": {
             "version": "1.4.0",
@@ -2359,7 +2481,6 @@
             "version": "5.1.0",
             "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
             "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
-            "dev": true,
             "requires": {
                 "mimic-fn": "^2.1.0"
             }
@@ -2391,6 +2512,45 @@
             "resolved": "https://registry.npmjs.org/opts/-/opts-1.2.7.tgz",
             "integrity": "sha512-hwZhzGGG/GQ7igxAVFOEun2N4fWul31qE9nfBdCnZGQCB5+L7tN9xZ+94B4aUpLOJx/of3zZs5XsuubayQYQjA=="
         },
+        "ora": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/ora/-/ora-4.0.3.tgz",
+            "integrity": "sha512-fnDebVFyz309A73cqCipVL1fBZewq4vwgSHfxh43vVy31mbyoQ8sCH3Oeaog/owYOs/lLlGVPCISQonTneg6Pg==",
+            "requires": {
+                "chalk": "^3.0.0",
+                "cli-cursor": "^3.1.0",
+                "cli-spinners": "^2.2.0",
+                "is-interactive": "^1.0.0",
+                "log-symbols": "^3.0.0",
+                "mute-stream": "0.0.8",
+                "strip-ansi": "^6.0.0",
+                "wcwidth": "^1.0.1"
+            },
+            "dependencies": {
+                "chalk": {
+                    "version": "3.0.0",
+                    "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+                    "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+                    "requires": {
+                        "ansi-styles": "^4.1.0",
+                        "supports-color": "^7.1.0"
+                    }
+                },
+                "has-flag": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+                    "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+                },
+                "supports-color": {
+                    "version": "7.1.0",
+                    "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+                    "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+                    "requires": {
+                        "has-flag": "^4.0.0"
+                    }
+                }
+            }
+        },
         "os-tmpdir": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@@ -2600,7 +2760,6 @@
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
             "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
-            "dev": true,
             "requires": {
                 "onetime": "^5.1.0",
                 "signal-exit": "^3.0.2"
@@ -2678,8 +2837,7 @@
         "signal-exit": {
             "version": "3.0.3",
             "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
-            "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
-            "dev": true
+            "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
         },
         "slice-ansi": {
             "version": "2.1.0",
@@ -2858,12 +3016,18 @@
             "dev": true
         },
         "supports-color": {
-            "version": "5.5.0",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-            "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-            "dev": true,
+            "version": "7.1.0",
+            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+            "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
             "requires": {
-                "has-flag": "^3.0.0"
+                "has-flag": "^4.0.0"
+            },
+            "dependencies": {
+                "has-flag": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+                    "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+                }
             }
         },
         "table": {
@@ -3047,6 +3211,14 @@
             "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
             "dev": true
         },
+        "wcwidth": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+            "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
+            "requires": {
+                "defaults": "^1.0.3"
+            }
+        },
         "whatwg-fetch": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
diff --git a/package.json b/package.json
index 27004d0..3c90c97 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
     },
     "scripts": {
         "pretest": "npm install --no-save ngrok",
-        "test": "nyc mocha test/**/*.test.js && test/install/test-npm-install.sh",
+        "test": "WSKDEBUG_SILENT=1 nyc mocha test/**/*.test.js && test/install/test-npm-install.sh",
         "posttest": "eslint .",
         "report-coverage": "nyc report --reporter=json && codecov -f coverage/coverage-final.json"
     },
@@ -42,6 +42,7 @@
         "file": "test/logfile.setup.js"
     },
     "dependencies": {
+        "chalk": "^4.0.0",
         "clone": "^2.1.2",
         "debug": "^4.1.1",
         "fetch-retry": "^3.1.0",
@@ -50,6 +51,7 @@
         "livereload": "^0.9.1",
         "manakin": "^0.5.2",
         "openwhisk": "^3.21.1",
+        "ora": "^4.0.3",
         "pretty-bytes": "^5.3.0",
         "pretty-ms": "^6.0.1",
         "yargs": "^15.3.1"
diff --git a/src/agentmgr.js b/src/agentmgr.js
index f96c6fa..3905847 100644
--- a/src/agentmgr.js
+++ b/src/agentmgr.js
@@ -27,8 +27,8 @@
 
 const fs = require('fs-extra');
 const sleep = require('util').promisify(setTimeout);
-const debug = require('./debug');
 const clone = require('clone');
+const log = require('./log');
 
 function getAnnotation(action, key) {
     const a = action.annotations.find(a => a.key === key);
@@ -78,7 +78,7 @@
     if (await actionExists(wsk, name)) {
         await wsk.actions.delete(name);
     }
-    debug(`restore: ensured removal of action ${name}`);
+    log.debug(`restore: ensured removal of action ${name}`);
 }
 
 
@@ -99,9 +99,6 @@
      * Fast way to get just the action metadata
      */
     async peekAction() {
-        if (this.argv.verbose) {
-            console.log(`Getting action metadata from OpenWhisk: ${this.actionName}`);
-        }
         let action = await getWskActionWithoutCode(this.wsk, this.actionName);
         if (action === null) {
             throw new Error(`Action not found: ${this.actionName}`);
@@ -126,7 +123,7 @@
                     throw new Error(`Dang! Agent is already installed and action backup is broken (${backupName}).\n\nPlease redeploy your action first before running wskdebug again.`);
 
                 } else {
-                    console.warn("Agent was already installed, but backup is still present. All good.");
+                    log.warn("Agent was already installed, but backup is still present. All good.");
 
                     // need to look at the original action
                     action = backup;
@@ -148,14 +145,10 @@
     }
 
     async readActionWithCode() {
-        if (this.argv.verbose) {
-            console.log(`Fetching action code from OpenWhisk: ${this.actionName}`);
-        }
-
         // user can switch between agents (ngrok or not), hence we need to restore first
         // (better would be to track the agent + its version and avoid a restore, but that's TBD)
         if (this.agentInstalled) {
-            this.actionWithCode = await this.restoreAction();
+            this.actionWithCode = await this.restoreAction(true);
         } else {
             this.actionWithCode = await this.wsk.actions.get(this.actionName);
         }
@@ -167,7 +160,7 @@
         return this.actionWithCode;
     }
 
-    async installAgent(invoker, debugTask) {
+    async installAgent(invoker, debug2) {
         this.agentInstalled = true;
 
         let agentName;
@@ -189,15 +182,17 @@
             // agent using ngrok for forwarding
             agentName = "ngrok";
             agentCode = await this.ngrokAgent.getAgent(agentAction);
-            debugTask("started local ngrok proxy");
+            debug2("started local ngrok proxy");
 
         } else {
             if (this.argv.disableConcurrency) {
                 this.concurrency = false;
+
             } else {
                 this.concurrency = await this.openwhiskSupports("concurrency");
+                debug2(`fetched openwhisk /api/v1/api-docs to detect concurrency`);
                 if (!this.concurrency) {
-                    console.warn("This OpenWhisk does not support action concurrency. Debugging will be a bit slower. Consider using '--ngrok' which might be a faster option.");
+                    log.warn("This OpenWhisk does not support action concurrency. Debugging will be a bit slower. Consider using '--ngrok' which might be a faster option.");
                 }
             }
 
@@ -214,26 +209,18 @@
 
         const backupName = getActionCopyName(this.actionName);
 
-        if (this.argv.verbose) {
-            console.log(`Installing agent in OpenWhisk (${agentName})...`);
-        }
-
         // create copy in case wskdebug gets killed hard
         // do async as this can be slow for larger actions and this is part of the critical startup path
         this.createBackup = (async () => {
-            const debugTask = debug.task();
+            const debug3 = log.newDebug();
 
             await this.wsk.actions.update({
                 name: backupName,
                 action: agentAction
             });
-            debugTask(`created action backup ${backupName}`);
+            debug3(`created action backup ${backupName}`);
         })();
 
-        if (this.argv.verbose) {
-            console.log(`Original action will be backed up at ${backupName}.`);
-        }
-
         if (this.argv.condition) {
             agentAction.parameters.push({
                 key: "$condition",
@@ -246,17 +233,13 @@
         } catch (e) {
             // openwhisk does not support concurrent nodejs actions, try with another
             if (e.statusCode === 400 && e.error && typeof e.error.error === "string" && e.error.error.includes("concurrency")) {
-                console.log(`The Openwhisk server does not support concurrent actions, using alternative agent. Consider using --ngrok for a possibly faster agent.`);
+                log.log(`The Openwhisk server does not support concurrent actions, using alternative agent. Consider using --ngrok for a possibly faster agent.`);
                 this.concurrency = false;
                 agentCode = await this.getPollingActivationDbAgent();
                 await this.pushAgent(agentAction, agentCode, backupName);
             }
         }
-        debugTask(`installed agent '${agentName}' in place of ${this.actionName}`);
-
-        if (this.argv.verbose) {
-            console.log(`Agent installed.`);
-        }
+        debug2(`installed agent type '${agentName}' in place of action '${this.actionName}'`);
     }
 
     stop() {
@@ -276,7 +259,7 @@
         } finally {
             if (this.ngrokAgent) {
                 await this.ngrokAgent.stop();
-                debug("ngrok shut down");
+                log.debug("ngrok shut down");
             }
         }
     }
@@ -302,9 +285,8 @@
                         blocking: true
                     });
 
-                    if (this.argv.verbose) {
-                        process.stdout.write(".");
-                    }
+                    log.verboseWrite(".");
+
                 } else {
                     // poll for the newest activation
                     const since = Date.now();
@@ -339,18 +321,14 @@
                             }
                         }
 
-                        if (this.argv.verbose) {
-                            process.stdout.write(".");
-                        }
+                        log.verboseWrite(".");
 
                         // need to limit load on openwhisk (activation list)
                         await sleep(1000);
                     }
                 }
 
-                if (this.argv.verbose) {
-                    process.stdout.write(".");
-                }
+                log.verboseWrite(".");
 
                 // check for successful response with a new activation
                 if (activation && activation.response) {
@@ -359,13 +337,9 @@
                     // mark this as seen so we don't reinvoke it
                     this.activationsSeen[activation.activationId] = true;
 
-                    if (this.argv.verbose) {
-                        console.log();
-                        console.info(`Activation: ${params.$activationId}`);
-                        console.log(params);
-                    } else {
-                        console.info(`Activation: ${params.$activationId}`);
-                    }
+                    log.verbose(); // because of the .....
+                    log.log();
+                    log.highlight("Activation: ", params.$activationId);
                     return params;
 
                 } else if (activation && activation.activationId) {
@@ -377,7 +351,7 @@
 
                 } else {
                     // unexpected, just log and retry
-                    console.log("Unexpected empty response while waiting for new activations:", activation);
+                    log.log("Unexpected empty response while waiting for new activations:", activation);
                 }
 
             } catch (e) {
@@ -385,30 +359,26 @@
                 const errorCode = getActivationError(e).code;
                 if (errorCode === 42) {
                     // 42 => retry, do nothing here (except logging progress)
-                    if (this.argv.verbose) {
-                        process.stdout.write(".");
-                    }
+                    log.verboseWrite(".");
 
                 } else if (errorCode === 43) {
                     // 43 => graceful shutdown (for unit tests)
-                    console.log("Graceful shutdown requested by agent (only for unit tests)");
+                    log.log("Graceful shutdown requested by agent (only for unit tests)");
                     return null;
 
                 } else if (e.statusCode === 503 && !this.concurrency) {
                     // 503 => openwhisk activation DB likely overloaded with requests, warn, wait a bit and retry
 
-                    if (this.argv.verbose) {
-                        console.log("x");
-                    }
-                    console.warn("Server responded with 503 while looking for new activation records. Consider using --ngrok option.")
+                    log.verbose("x");
+                    log.warn("Server responded with 503 while looking for new activation records. Consider using --ngrok option.")
 
                     await sleep(5000);
 
                 } else {
                     // otherwise log error and abort
-                    console.error();
-                    console.error("Unexpected error while polling agent for activation:");
-                    console.dir(e, { depth: null });
+                    log.error();
+                    log.error("Unexpected error while polling agent for activation:");
+                    log.deepObject(e);
                     throw new Error("Unexpected error while polling agent for activation.");
                 }
             }
@@ -419,10 +389,8 @@
     }
 
     async completeActivation(activationId, result, duration) {
-        console.info(`Completed activation ${activationId} in ${duration/1000.0} sec`);
-        if (this.argv.verbose) {
-            console.log(result);
-        }
+        log.succeed(`Completed activation ${activationId} in ` + log.highlightColor(`${duration/1000.0} sec`));
+        log.verbose("Result:", result);
 
         try {
             result.$activationId = activationId;
@@ -439,10 +407,10 @@
                 // do nothing
             } else if (errorCode === 43) {
                 // 43 => graceful shutdown (for unit tests)
-                console.log("Graceful shutdown requested by agent (only for unit tests)");
+                log.log("Graceful shutdown requested by agent (only for unit tests)");
                 return false;
             } else {
-                console.error("Unexpected error while completing activation:", e);
+                log.error("Unexpected error while completing activation:", e);
             }
         }
         return true;
@@ -450,12 +418,7 @@
 
     // --------------------------------------< restoring >------------------
 
-    async restoreAction() {
-        if (this.argv.verbose) {
-            console.log();
-            console.log(`Restoring action`);
-        }
-
+    async restoreAction(isStartup) {
         const copy = getActionCopyName(this.actionName);
 
         try {
@@ -470,7 +433,7 @@
             } else {
                 // the original was fetched before or was backed up in the copy
                 original = await this.wsk.actions.get(copy)
-                debug("restore: fetched action original from backup copy");
+                log.debug("restore: fetched action original from backup copy");
             }
 
             // copy the backup (copy) to the regular action
@@ -478,31 +441,34 @@
                 name: this.actionName,
                 action: original
             });
-            debug("restore: restored original action");
+            log.debug("restore: restored original action");
 
             if (this.argv.cleanup) {
-                console.log("Removing extra actions due to --cleanup...");
+                if (!isStartup) {
+                    log.log("Removing helper actions due to --cleanup...");
+                }
                 // remove the backup
                 await this.wsk.actions.delete(copy);
-                debug("restore: deleted backup copy");
+                log.debug("restore: deleted backup copy");
 
                 // remove any helpers if they exist
                 await deleteActionIfExists(this.wsk, `${this.actionName}_wskdebug_invoked`);
                 await deleteActionIfExists(this.wsk, `${this.actionName}_wskdebug_completed`);
 
-            } else {
-                console.warn(`Skipping removal of extra actions. Remove using --cleanup if desired:`);
-                console.warn(`- ${copy}`);
-                if (!this.concurrency) {
-                    console.warn(`- ${this.actionName}_wskdebug_invoked`);
-                    console.warn(`- ${this.actionName}_wskdebug_completed`);
+            } else if (!isStartup) {
+                log.log(`Following helper actions are not removed to make shutdown fast. Remove using --cleanup if desired.`);
+                log.log(`- ${log.highlightColor(copy)}`);
+                if (!this.concurrency && !this.ngrokAgent) {
+                    log.log("- " + log.highlightColor(`${this.actionName}_wskdebug_invoked`));
+                    log.log("- " + log.highlightColor(`${this.actionName}_wskdebug_completed`));
                 }
+                log.log();
             }
 
             return original;
 
         } catch (e) {
-            console.error("Error while restoring original action:", e);
+            log.error("Error while restoring original action:", e);
         }
     }
 
@@ -554,7 +520,7 @@
                     { key: "wskdebug", value: true },
                     { key: "description", value: `wskdebug agent. temporarily installed over original action. original action backup at ${backupName}.` }
                 ],
-                parameters: action.parameters
+                parameters: action.parameters || []
             }
         });
     }
@@ -578,7 +544,7 @@
                 ]
             }
         });
-        debug(`created helper action ${actionName}`);
+        log.debug(`created helper action ${actionName}`);
     }
 
     // ----------------------------------------< openwhisk feature detection >-----------------
@@ -593,7 +559,7 @@
                     this.openwhiskVersion = null;
                 }
             } catch (e) {
-                console.warn("Could not retrieve OpenWhisk version:", e.message);
+                log.warn("Could not retrieve OpenWhisk version:", e.message);
                 this.openwhiskVersion = null;
             }
         }
@@ -615,7 +581,7 @@
                         return swagger.definitions.ActionLimits.properties.concurrency;
                     }
                 } catch (e) {
-                    console.warn('Could not read /api/v1/api-docs, setting max action concurrency to 1')
+                    log.warn('Could not read /api/v1/api-docs, setting max action concurrency to 1')
                     return false;
                 }
             }
diff --git a/src/agents/ngrok.js b/src/agents/ngrok.js
index 1ee2a45..9a1b420 100644
--- a/src/agents/ngrok.js
+++ b/src/agents/ngrok.js
@@ -23,6 +23,7 @@
 const url = require('url');
 const util = require('util');
 const crypto = require("crypto");
+const log = require('../log');
 
 class NgrokAgent {
     constructor(argv, invoker) {
@@ -31,9 +32,7 @@
     }
 
     async getAgent(action) {
-        if (this.argv.verbose) {
-            console.log("Setting up ngrok", this.argv.ngrokRegion ? `(region: ${this.argv.ngrokRegion})` : "");
-        }
+        log.verbose("Setting up ngrok", this.argv.ngrokRegion ? `(region: ${this.argv.ngrokRegion})` : "");
 
         // 1. start local server on random port
         this.ngrokServer = http.createServer(this.ngrokHandler.bind(this));
@@ -62,7 +61,9 @@
             value: this.ngrokAuth
         });
 
-        console.log(`Ngrok forwarding: ${ngrokUrl} => http://localhost:${this.ngrokServerPort} (auth: ${this.ngrokAuth})`);
+        const h = log.highlightColor;
+        log.step(`Ngrok forwarding: ${h(ngrokUrl)} => http://localhost:${h(this.ngrokServerPort)}`);
+        log.debug(`ngrok agent auth key: ${this.ngrokAuth}`)
 
         return fs.readFileSync(`${__dirname}/../../agent/agent-ngrok.js`, {encoding: 'utf8'});
     }
@@ -99,33 +100,28 @@
             req.on('end', async () => {
                 try {
                     const params = JSON.parse(body);
-                    const id = params.$activationId;
+                    const activationId = params.$activationId;
                     delete params.$activationId;
 
-                    if (this.argv.verbose) {
-                        console.log();
-                        console.info(`Activation: ${id}`);
-                        console.log(params);
-                    } else {
-                        console.info(`Activation: ${id}`);
-                    }
+                    log.verbose(); // because of the .....
+                    log.log();
+                    log.highlight("Activation: ", activationId);
+                    log.verbose("Parameters:", params);
 
                     const startTime = Date.now();
 
-                    const result = await this.invoker.run(params, id);
+                    const result = await this.invoker.run(params, activationId);
 
                     const duration = Date.now() - startTime;
-                    console.info(`Completed activation ${id} in ${duration/1000.0} sec`);
-                    if (this.argv.verbose) {
-                        console.log(result);
-                    }
+                    log.succeed(`Completed activation ${activationId} in ` + log.highlightColor(`${duration/1000.0} sec`));
+                    log.verbose("Result:", result);
 
                     res.statusCode = 200;
                     res.setHeader("Content-Type", "application/json");
                     res.end(JSON.stringify(result));
 
                 } catch (e) {
-                    console.error(e);
+                    log.error(e);
                     res.statusCode = 400;
                     res.end();
                 }
diff --git a/src/debug.js b/src/debug.js
deleted file mode 100644
index 17ac7b9..0000000
--- a/src/debug.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-'use strict';
-
-// common debug() instance for shared time spent measurments (+millis)
-module.exports = require('debug')('wskdebug');
-
-// start a sub debug instance for logging times in parallel promises
-module.exports.task = () => {
-    const debug = require('debug')('wskdebug')
-    // trick to start time measurement from now on without logging an extra line
-    debug.log = () => {};
-    debug();
-    delete debug.log;
-    return debug;
-}
diff --git a/src/debugger.js b/src/debugger.js
index 879d497..e7d250b 100644
--- a/src/debugger.js
+++ b/src/debugger.js
@@ -24,9 +24,17 @@
 const openwhisk = require('openwhisk');
 const { spawnSync } = require('child_process');
 const sleep = require('util').promisify(setTimeout);
-const debug = require('./debug');
 const prettyBytes = require('pretty-bytes');
 const prettyMilliseconds = require('pretty-ms');
+const log = require('./log');
+
+function prettyMBytes1024(mb) {
+    if (mb > 1024) {
+        return `${mb/1024} GB`;
+    } else {
+        return `${mb} MB`;
+    }
+}
 
 /**
  * Central component of wskdebug.
@@ -34,14 +42,14 @@
 class Debugger {
     constructor(argv) {
         this.startTime = Date.now();
-        debug("starting debugger");
+        log.debug("starting debugger");
 
         this.argv = argv;
         this.actionName = argv.action;
 
         this.wskProps = wskprops.get();
         if (Object.keys(this.wskProps).length === 0) {
-            console.error(`Error: Missing openwhisk credentials. Found no ~/.wskprops file or WSK_* environment variable.`);
+            log.error(`Error: Missing openwhisk credentials. Found no ~/.wskprops file or WSK_* environment variable.`);
             process.exit(1);
         }
         if (argv.ignoreCerts) {
@@ -51,9 +59,12 @@
         try {
             this.wsk = openwhisk(this.wskProps);
         } catch (err) {
-            console.error(`Error: Could not setup openwhisk client: ${err.message}`);
+            log.error(`Error: Could not setup openwhisk client: ${err.message}`);
             process.exit(1);
         }
+
+        const h = log.highlightColor;
+        log.spinner("Debugging " + h(`/_/${this.actionName}`) + " on " + h(this.wskProps.apihost));
     }
 
     async start() {
@@ -61,19 +72,20 @@
         this.watcher = new Watcher(this.argv, this.wsk);
 
         // get the action metadata
-        const actionMetadata = await this.agentMgr.peekAction();
-        debug("fetched action metadata from openwhisk");
+        this.actionMetadata = await this.agentMgr.peekAction();
+        log.debug("fetched action metadata from openwhisk");
+        this.wskProps.namespace = this.actionMetadata.namespace;
 
-        this.wskProps.namespace = actionMetadata.namespace;
-        console.info(`Starting debugger for /${this.wskProps.namespace}/${this.actionName}`);
+        const h = log.highlightColor;
+        log.step("Debugging " + h(`/${this.wskProps.namespace}/${this.actionName}`) + " on " + h(this.wskProps.apihost));
 
         // local debug container
-        this.invoker = new OpenWhiskInvoker(this.actionName, actionMetadata, this.argv, this.wskProps, this.wsk);
+        this.invoker = new OpenWhiskInvoker(this.actionName, this.actionMetadata, this.argv, this.wskProps, this.wsk);
 
         try {
             // run build initially (would be required by starting container)
             if (this.argv.onBuild) {
-                console.info("=> Build:", this.argv.onBuild);
+                log.highlight("On build: ", this.argv.onBuild);
                 spawnSync(this.argv.onBuild, {shell: true, stdio: "inherit"});
             }
             await this.invoker.prepare();
@@ -82,20 +94,21 @@
 
             // task 1 - start local container
             const containerTask = (async () => {
-                const debugTask = debug.task();
+                const debug2 = log.newDebug();
+                log.spinner('Starting local container');
 
                 // start container - get it up fast for VSCode to connect within its 10 seconds timeout
-                await this.invoker.startContainer();
+                await this.invoker.startContainer(debug2);
 
-                debugTask(`started container: ${this.invoker.name()}`);
+                debug2(`started container: ${this.invoker.name()}`);
             })();
 
             // task 2 - fetch action code from openwhisk
             const openwhiskTask = (async () => {
-                const debugTask = debug.task();
+                const debug2 = log.newDebug();
                 const actionWithCode = await this.agentMgr.readActionWithCode();
 
-                debugTask(`got action code (${prettyBytes(actionWithCode.exec.code.length)})`);
+                debug2(`downloaded action code (${prettyBytes(actionWithCode.exec.code.length)})`);
                 return actionWithCode;
             })();
 
@@ -103,51 +116,41 @@
             const results = await Promise.all([containerTask, openwhiskTask]);
             const actionWithCode = results[1];
 
+            log.spinner('Installing agent');
+
             // parallelize slower work using promises again
 
             // task 3 - initialize local container with code
             const initTask = (async () => {
-                const debugTask = debug.task();
+                const debug2 = log.newDebug();
 
                 // /init local container
                 await this.invoker.init(actionWithCode);
 
-                debugTask("installed action on container");
+                debug2("installed action on container");
             })();
 
             // task 4 - install agent in openwhisk
             const agentTask = (async () => {
-                const debugTask = debug.task();
+                const debug2 = log.newDebug();
 
                 // setup agent in openwhisk
-                await this.agentMgr.installAgent(this.invoker, debugTask);
+                await this.agentMgr.installAgent(this.invoker, debug2);
             })();
 
             await Promise.all([initTask, agentTask]);
 
             if (this.argv.onStart) {
-                console.log("On start:", this.argv.onStart);
+                log.highlight("On start: ", this.argv.onStart);
                 spawnSync(this.argv.onStart, {shell: true, stdio: "inherit"});
             }
 
             // start source watching (live reload) if requested
             await this.watcher.start();
 
-            console.log();
-            console.info(`Action     : /${this.wskProps.namespace}/${this.actionName}`);
-            if (this.sourcePath) {
-                console.info(`Sources    : ${this.invoker.getSourcePath()}`);
-            }
-            console.info(`Image      : ${this.invoker.getImage()}`);
-            console.info(`OpenWhisk  : ${this.wskProps.apihost}`);
-            console.info(`Container  : ${this.invoker.name()}`);
-            console.info(`Debug type : ${this.invoker.getDebugKind()}`);
-            console.info(`Debug port : localhost:${this.invoker.getPort()}`);
-            if (this.argv.condition) {
-                console.info(`Condition  : ${this.argv.condition}`);
-            }
-            console.log();
-            console.info(`Ready for activations. Started in ${prettyMilliseconds(Date.now() - this.startTime)}. Use CTRL+C to exit`);
+            this.logDetails();
+            const abortMsg = log.isInteractive ? log.highlightColor(" Use CTRL+C to exit.") : "";
+            log.ready(`Ready for activations. Started in ${prettyMilliseconds(Date.now() - this.startTime)}.${abortMsg}`);
 
             this.ready = true;
 
@@ -157,6 +160,30 @@
         }
     }
 
+    async logDetails() {
+        log.log();
+        log.highlight("Action     : ", `/${this.wskProps.namespace}/${this.actionName}`);
+        if (this.sourcePath) {
+            log.highlight("Sources    : ", `${this.invoker.getSourcePath()}`);
+        }
+        log.highlight("Image      : ", `${this.invoker.getImage()}`);
+        log.highlight("Container  : ", `${this.invoker.name()}`);
+        if (this.actionMetadata.limits) {
+            if (this.actionMetadata.limits.memory) {
+                log.highlight("Memory     : ", `${prettyMBytes1024(this.actionMetadata.limits.memory)}`);
+            }
+            if (this.actionMetadata.limits.timeout) {
+                log.highlight("Timeout    : ", `${prettyMilliseconds(this.actionMetadata.limits.timeout, {verbose:true})}`);
+            }
+        }
+        log.highlight("Debug type : ", `${this.invoker.getDebugKind()}`);
+        log.highlight("Debug port : ", `localhost:${this.invoker.getPort()}`);
+        if (this.argv.condition) {
+            log.highlight("Condition  : ", `${this.argv.condition}`);
+        }
+        log.log();
+    }
+
     async run() {
         return this.runPromise = this._run();
     }
@@ -164,7 +191,6 @@
     async _run() {
         try {
             this.running = true;
-            this.shuttingDown = false;
 
             // main blocking loop
             // abort if this.running is set to false
@@ -186,6 +212,7 @@
 
                     const id = activation.$activationId;
                     delete activation.$activationId;
+                    log.verbose("Parameters:", activation);
 
                     const startTime = Date.now();
 
@@ -234,18 +261,25 @@
 
     async shutdown() {
         // avoid duplicate shutdown on CTRL+C
-        if (this.shuttingDown) {
-            return;
+        if (!this.shutdownPromise) {
+            this.shutdownPromise = this._shutdown();
         }
-        this.shuttingDown = true;
+
+        await this.shutdownPromise;
+        delete this.shutdownPromise;
+    }
+
+    async _shutdown() {
         const shutdownStart = Date.now();
-        debug("shutting down...");
 
         // only log this if we started properly
         if (this.ready) {
-            console.log();
-            console.log();
-            console.log("Shutting down...");
+            log.log();
+            log.log();
+            log.debug("shutting down...");
+            log.spinner("Shutting down");
+        } else {
+            log.debug("aborting start - shutting down ...");
         }
 
         // need to shutdown everything even if some fail, hence tryCatch() for each
@@ -255,23 +289,23 @@
         }
         if (this.invoker) {
             await this.tryCatch(this.invoker.stop());
-            debug(`stopped container: ${this.invoker.name()}`);
+            log.debug(`stopped container: ${this.invoker.name()}`);
         }
         if (this.watcher) {
             await this.tryCatch(this.watcher.stop());
-            debug("stopped source file watching");
+            log.debug("stopped source file watching");
         }
 
         // only log this if we started properly
         if (this.ready) {
-            console.log(`Done (shutdown took ${prettyMilliseconds(Date.now() - shutdownStart)})`);
+            log.succeed(`Done. Shutdown in ${prettyMilliseconds(Date.now() - shutdownStart)}.`);
         }
         this.ready = false;
     }
 
     // ------------------------------------------------< utils >-----------------
 
-    async tryCatch(task, message="Error during shutdown:") {
+    async tryCatch(task) {
         try {
             if (typeof task === "function") {
                 task();
@@ -279,13 +313,7 @@
                 await task;
             }
         } catch (e) {
-            console.log(e);
-            if (this.argv.verbose) {
-                console.error(message);
-                console.error(e);
-            } else {
-                console.error(message, e.message);
-            }
+            log.exception(e, "Error during shutdown:");
         }
     }
 
diff --git a/src/invoker.js b/src/invoker.js
index eed849a..32b9d65 100644
--- a/src/invoker.js
+++ b/src/invoker.js
@@ -21,6 +21,7 @@
 const fetch = require('fetch-retry')(require('isomorphic-fetch'));
 const kinds = require('./kinds/kinds');
 const path = require('path');
+const log = require("./log");
 
 const RUNTIME_PORT = 8080;
 const INIT_RETRY_DELAY_MS = 100;
@@ -31,12 +32,11 @@
     memory: 256
 };
 
-function execute(cmd, options, verbose) {
+function execute(cmd, options, debug2) {
     cmd = cmd.replace(/\s+/g, ' ');
-    if (verbose) {
-        console.log(cmd);
-    }
     const result = execSync(cmd, options);
+
+    (debug2 || log.debug)(`executed: ${cmd}`);
     if (result) {
         return result.toString().trim();
     } else {
@@ -65,7 +65,6 @@
         this.internalPort = options.internalPort;
         this.command = options.command;
         this.dockerArgs = options.dockerArgs;
-        this.verbose = options.verbose;
 
         // the build path can be separate, if not, same as the source/watch path
         this.sourcePath = options.buildPath || options.sourcePath;
@@ -107,14 +106,12 @@
                 }
                 return runtimes[kind];
 
-            } else if (this.verbose) {
-                console.warn("Could not retrieve runtime images from OpenWhisk, using default image list.");
+            } else {
+                log.warn("Could not retrieve runtime images from OpenWhisk, using default image list.");
             }
 
         } catch (e) {
-            if (this.verbose) {
-                console.warn("Could not retrieve runtime images from OpenWhisk, using default image list.", e.message);
-            }
+            log.warn("Could not retrieve runtime images from OpenWhisk, using default image list.", e.message);
         }
         return kinds.images[kind];
     }
@@ -146,9 +143,7 @@
         try {
             this.debug = require(`${__dirname}/kinds/${this.debugKind}/${this.debugKind}`);
         } catch (e) {
-            if (this.verbose) {
-                console.error(`Cannot find debug info for kind ${this.debugKind}:`, e.message);
-            }
+            log.warn(`Cannot find debug info for kind ${this.debugKind}:`, e.message);
             this.debug = {};
         }
 
@@ -175,7 +170,7 @@
         // source mounting
         if (this.sourcePath) {
             if (!this.debug.mountAction) {
-                console.warn(`Warning: Sorry, mounting sources not yet supported for: ${kind}.`);
+                log.warn(`Warning: Sorry, mounting sources not yet supported for: ${kind}.`);
                 this.sourcePath = undefined;
             }
         }
@@ -188,8 +183,8 @@
         }
     }
 
-    async startContainer() {
-        let showDockerRunOutput = this.verbose;
+    async startContainer(debug2) {
+        let showDockerRunOutput = log.isVerbose;
 
         // quick fail for missing requirements such as docker not running
         await this.checkIfDockerAvailable();
@@ -199,7 +194,7 @@
         } catch (e) {
             // make sure the user can see the image download process as part of docker run
             showDockerRunOutput = true;
-            console.log(`
+            log.warn(`
 +------------------------------------------------------------------------------------------+
 | Docker image must be downloaded: ${this.image}
 |                                                                                          |
@@ -213,10 +208,6 @@
 `);
         }
 
-        if (this.verbose) {
-            console.log(`Starting local debug container ${this.name()}`);
-        }
-
         execute(
             `docker run
                 -d
@@ -232,11 +223,12 @@
             `,
             // live stream view for docker image download output
             { stdio: showDockerRunOutput ? "inherit" : null },
-            this.verbose
+            debug2
         );
 
         this.containerRunning = true;
 
+        log.stopSpinner();
         spawn("docker", ["logs", "-t", "-f", this.name()], {
             stdio: [
                 "inherit", // stdin
@@ -265,16 +257,9 @@
     async init(actionWithCode) {
         let action;
         if (this.sourceMountAction) {
-            if (this.verbose) {
-                console.log(`Mounting sources onto local debug container: ${this.sourcePath}`);
-            }
-
             action = this.sourceMountAction;
 
         } else {
-            if (this.verbose) {
-                console.log(`Pushing action code to local debug container: ${this.action.name}`);
-            }
             action = {
                 binary: actionWithCode.exec.binary,
                 main:   actionWithCode.exec.main || "main",
@@ -324,9 +309,6 @@
 
     async stop() {
         if (this.containerRunning) {
-            if (this.verbose) {
-                console.log("Stopping local debug container");
-            }
             execute(`docker kill ${this.name()}`);
         }
     }
diff --git a/src/log.js b/src/log.js
new file mode 100644
index 0000000..fc39e25
--- /dev/null
+++ b/src/log.js
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const dbg = require('debug');
+const ora = require('ora');
+const chalk = require('chalk');
+
+const INFO_COLOR_ANSI = 36;
+const infoColor = chalk.ansi(INFO_COLOR_ANSI);
+const highlightColor = chalk.magenta;
+
+const spinner = ora({
+    color: "cyan",
+    stream: process.stdout
+});
+
+const DEBUG_NAMESPACE = "wskdebug"
+const debug = dbg(DEBUG_NAMESPACE);
+
+const noop = () => {};
+
+// no emoji support in windows terminal
+const useEmoji = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color';
+const symbols = useEmoji ? {
+    step: '❯',
+    success: '✔',
+    ready: '🚀'
+} : {
+    step: '-',
+    success: '√',
+    ready: '>'
+};
+
+if (debug.enabled || !spinner.isEnabled) {
+    // disable spinner since we have all the debug() logs saying similar stuff
+    spinner.start = noop;
+    spinner.stop = noop;
+}
+
+let originalConsole = null;
+
+module.exports = {
+
+    isVerbose: false,
+
+    silent: function(silent) {
+        if (silent) {
+            // silent wins
+            this.isVerbose = false;
+            dbg.disable();
+
+            this.log = noop;
+            this.step = noop;
+            this.highlight = noop;
+            this.warn = noop;
+            this.verboseWrite = noop;
+            this.deepObject = noop;
+            spinner.start = noop;
+            spinner.stopAndPersist = noop;
+        }
+    },
+
+    /** Important step message, prefixed with symbol, visible by default. Ends any running spinner(). */
+    step: function(text) {
+        spinner.stopAndPersist({
+            symbol: infoColor(symbols.step),
+            text: spinner.isEnabled ? infoColor(text) : text
+        });
+    },
+
+    highlight: function(text, highlight) {
+        this.step(text + highlightColor(highlight));
+    },
+
+    highlightColor,
+
+    /** Basic log message, visible by default. Ends any running spinner(). */
+    log: function(...args) {
+        spinner.stop();
+        // goes to stdout
+        console.info(...args);
+    },
+
+    /** Warning message, visible by default. Ends any running spinner(). */
+    warn: function(...args) {
+        spinner.stop();
+        // goes to stderr
+        console.warn(...args);
+    },
+
+    /** Error message, visible by default. Ends any running spinner(). */
+    error: function(...args) {
+        spinner.stop();
+        // goes to stderr
+        console.error(...args);
+    },
+
+    verbose: function(...args) {
+        if (this.isVerbose) {
+            this.log(...args);
+        }
+    },
+
+    verboseWrite: function(text) {
+        if (this.isVerbose) {
+            process.stdout.write(text);
+        }
+    },
+
+    exception: function(err, message="Error:") {
+        // stacktrace only in verbose
+        if (this.isVerbose) {
+            this.error(err);
+        } else {
+            this.error(message, err.message);
+        }
+    },
+
+    deepObject: function(obj) {
+        console.dir(obj, { depth: null });
+    },
+
+    // common debug() instance for shared time spent measurments (+millis)
+    debug,
+
+    /**
+     * Create a new "child" debug instance for logging times in parallel promises
+     */
+    newDebug: function() {
+        const debug = dbg(DEBUG_NAMESPACE);
+        // trick to start time measurement from now on without logging an extra line
+        debug.log = () => {};
+        debug();
+        delete debug.log;
+        return debug;
+    },
+
+    /** Start a spinner on the console */
+    spinner: function(text) {
+        spinner.start(infoColor(text) + " ");
+    },
+
+    /** Stop a running spinner().  */
+    stopSpinner: function() {
+        spinner.stop();
+    },
+
+    /** Finish any running spinner and show a log message with a success symbol in front. */
+    succeed: function(text) {
+        spinner.stopAndPersist({
+            symbol: chalk.green(symbols.success),
+            text: infoColor(text)
+        });
+    },
+
+    /** Finish any running spinner and show a log message with a ready symbol in front. */
+    ready: function(text) {
+        spinner.stopAndPersist({
+            symbol: infoColor(symbols.ready),
+            text: infoColor(text)
+        });
+    },
+
+    enableConsoleColors: function() {
+        // colorful console.log() and co
+        if (!console._logToFile) {
+            originalConsole = {
+                log: console.log,
+                error: console.error,
+                info: console.info,
+                debug: console.debug
+            };
+            // overwrites console.*()
+            const manakin = require('manakin').global;
+            manakin.info.color = INFO_COLOR_ANSI;
+
+            // no bright as it might not look good on terminals with white background
+            //manakin.setBright();
+        }
+        return originalConsole;
+    },
+
+    resetConsoleColors: function() {
+        if (originalConsole) {
+            console.log = originalConsole.log;
+            console.error = originalConsole.error;
+            console.info = originalConsole.info;
+            console.debug = originalConsole.debug;
+        }
+    },
+
+    isInteractive: spinner.isEnabled
+};
+
+if (process.env.WSKDEBUG_SILENT) {
+    module.exports.silent(true);
+}
diff --git a/src/watcher.js b/src/watcher.js
index dcea5a6..45558b5 100644
--- a/src/watcher.js
+++ b/src/watcher.js
@@ -20,7 +20,7 @@
 const fs = require('fs-extra');
 const livereload = require('livereload');
 const { spawnSync } = require('child_process');
-const debug = require('./debug');
+const log = require('./log');
 
 class Watcher {
     constructor(argv, wsk) {
@@ -38,6 +38,8 @@
              || this.argv.invokeParams
              || this.argv.invokeAction )
         ) {
+            log.spinner('Initializing source watching');
+
             this.liveReloadServer = livereload.createServer({
                 port: this.argv.livereloadPort,
                 noListen: !this.argv.livereload,
@@ -55,9 +57,7 @@
                 try {
                     let result = [];
 
-                    if (argv.verbose) {
-                        console.log("File modified:", filepath);
-                    }
+                    log.verbose("File modified:", filepath);
 
                     // call original function if we are listening
                     if (argv.livereload) {
@@ -66,13 +66,13 @@
 
                     // run build command before invoke triggers below
                     if (argv.onBuild) {
-                        console.info("=> Build:", argv.onBuild);
+                        log.highlight("On build: ", argv.onBuild);
                         spawnSync(argv.onBuild, {shell: true, stdio: "inherit"});
                     }
 
                     // run shell command
                     if (argv.onChange) {
-                        console.info("=> Run:", argv.onChange);
+                        log.highlight("On run: ", argv.onChange);
                         spawnSync(argv.onChange, {shell: true, stdio: "inherit"});
                     }
 
@@ -91,22 +91,22 @@
                             name: action,
                             params: json
                         }).then(response => {
-                            console.info(`=> Invoked action ${action} with params ${argv.invokeParams}: ${response.activationId}`);
+                            log.step(`Invoked action ${action} with params ${argv.invokeParams}: ${response.activationId}`);
                         }).catch(err => {
-                            console.error("Error invoking action:", err);
+                            log.error("Error invoking action:", err);
                         });
                     }
 
                     return result;
                 } catch (e) {
-                    console.error(e);
+                    log.error(e);
                 }
             };
 
             if (this.argv.livereload) {
-                console.info(`LiveReload enabled for ${watch} on port ${this.liveReloadServer.config.port}`);
+                log.log(`LiveReload enabled for ${log.highlightColor(watch)} on port ${this.liveReloadServer.config.port}`);
             }
-            debug("started source file watching");
+            log.debug("started source file watching");
         }
     }
 
diff --git a/test/test.js b/test/test.js
index d81bee1..0aaeed6 100644
--- a/test/test.js
+++ b/test/test.js
@@ -42,9 +42,7 @@
 
 async function beforeEach() {
     process.env.WSK_CONFIG_FILE = path.join(process.cwd(), "test/wskprops");
-    // nock.recorder.rec({ enable_reqheaders_recording: true });
     openwhisk = nock(FAKE_OPENWHISK_SERVER);
-    // openwhisk.log(console.log);
     mockOpenwhiskSwagger(openwhisk);
 
     // save current working dir