Repository: serverless/serverless-azure-functions
Pull request #214 (2019-07-24 17:26:12)

feat: Spawn offline process with `sls offline` command
- [x] `sls offline` builds Azure Functions files and spawns `func host start` in the same shell
- [x] `sls offline start` spawns `func host start` in the same shell
- [x] `BaseService` has `spawn` function that spawns and waits for child process in shell (routes stdout and stderr to internal logger)
- [x] Using `mock-spawn` library to mock spawned child processes
- [x] Add invocation commands to config constants

Resolves [AB#569]

coveralls: 
[![Coverage Status](https://coveralls.io/builds/24772952/badge)](https://coveralls.io/builds/24772952)

Coverage decreased (-0.09%) to 84.338% when pulling **79bf67235de0c5e6ad758664f7df6a07f2e8cf6e on tabarlow/spawn-child** into **844ffb5c93224f34b986460937c9cd2015f5b5ee on dev**.

pjlittle: nice work, looks great :+1: 
wbreza: sls offline builds Azure Functions files and spawns func host start in the same shell
  sls offline start spawns func host start in the same shell
  BaseService has spawn function that spawns and waits for child process in shell (routes stdout and stderr to internal logger)
  Using mock-spawn library to mock spawned child processes
  Add invocation commands to config constants

Patch:
diff --git a/README.md b/README.md
index c6f018b..7bb28e5 100644
--- a/README.md
+++ b/README.md
@@ -34,17 +34,27 @@ $ npm i azure-functions-core-tools -g
 Then, at the root of your project directory, run:
 
 ```bash
-# Builds necessary function bindings files
+# Builds necessary function bindings files and starts the function app
 $ sls offline
-# Starts the function app
-$ npm start
 ```
 
 The `offline` process will generate a directory for each of your functions, which will contain a file titled `function.json`. This will contain a relative reference to your handler file & exported function from that file as long as they are referenced correctly in `serverless.yml`.
 
-The `npm start` script just runs `func host start`, but we included the `npm` script for ease of use.
+After the necessary files are generated, it will start the function app from within the same shell. For HTTP functions, the local URLs will be displayed in the console when the function app is initialized.
 
-To clean up files generated from the build, you can simply run:
+To simply start the function app *without* building the files, run:
+
+```bash
+$ sls offline start
+```
+
+To build the files *without* spawning the process to start the function app, run:
+
+```bash
+$ sls offline build
+```
+
+To clean up files generated from the build, run:
 
 ```bash
 sls offline cleanup
diff --git a/package-lock.json b/package-lock.json
index 755ec01..ea294ca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1794,7 +1794,6 @@
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
       "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
-      "optional": true,
       "requires": {
         "kind-of": "^3.0.2",
         "longest": "^1.0.1",
@@ -1967,7 +1966,6 @@
       "version": "4.10.1",
       "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
       "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
-      "optional": true,
       "requires": {
         "bn.js": "^4.0.0",
         "inherits": "^2.0.1",
@@ -2437,8 +2435,7 @@
     "bn.js": {
       "version": "4.11.8",
       "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
-      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
-      "optional": true
+      "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
     },
     "body-parser": {
       "version": "1.19.0",
@@ -2543,8 +2540,7 @@
     "brorand": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
-      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
-      "optional": true
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
     },
     "browser-process-hrtime": {
       "version": "0.1.3",
@@ -2573,7 +2569,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
       "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
-      "optional": true,
       "requires": {
         "buffer-xor": "^1.0.3",
         "cipher-base": "^1.0.0",
@@ -2610,7 +2605,6 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
       "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
-      "optional": true,
       "requires": {
         "bn.js": "^4.1.0",
         "randombytes": "^2.0.1"
@@ -2707,8 +2701,7 @@
     "buffer-xor": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
-      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
-      "optional": true
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
     },
     "builtin-modules": {
       "version": "1.1.1",
@@ -2895,7 +2888,6 @@
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
       "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -3273,7 +3265,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
       "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
-      "optional": true,
       "requires": {
         "cipher-base": "^1.0.1",
         "inherits": "^2.0.1",
@@ -3286,7 +3277,6 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
       "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
-      "optional": true,
       "requires": {
         "cipher-base": "^1.0.3",
         "create-hash": "^1.1.0",
@@ -3750,7 +3740,6 @@
       "version": "6.4.1",
       "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
       "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
-      "optional": true,
       "requires": {
         "bn.js": "^4.4.0",
         "brorand": "^1.0.1",
@@ -3812,7 +3801,6 @@
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
       "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
-      "optional": true,
       "requires": {
         "prr": "~1.0.1"
       }
@@ -4192,7 +4180,6 @@
       "version": "0.3.5",
       "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
       "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
-      "optional": true,
       "requires": {
         "d": "1",
         "es5-ext": "~0.10.14"
@@ -4207,7 +4194,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
       "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
-      "optional": true,
       "requires": {
         "md5.js": "^1.3.4",
         "safe-buffer": "^5.1.1"
@@ -5505,7 +5491,6 @@
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
       "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -5515,7 +5500,6 @@
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
       "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.3",
         "minimalistic-assert": "^1.0.1"
@@ -5525,7 +5509,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
       "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
-      "optional": true,
       "requires": {
         "hash.js": "^1.0.3",
         "minimalistic-assert": "^1.0.0",
@@ -5844,8 +5827,7 @@
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-      "optional": true
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
     },
     "is-fullwidth-code-point": {
       "version": "1.0.0",
@@ -7485,8 +7467,7 @@
     "longest": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
-      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
-      "optional": true
+      "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
     },
     "loose-envify": {
       "version": "1.4.0",
@@ -7587,7 +7568,6 @@
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
       "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
-      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1",
@@ -7613,7 +7593,6 @@
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
       "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
-      "optional": true,
       "requires": {
         "errno": "^0.1.3",
         "readable-stream": "^2.0.1"
@@ -7704,14 +7683,12 @@
     "minimalistic-assert": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
-      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
-      "optional": true
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
     },
     "minimalistic-crypto-utils": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
-      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
-      "optional": true
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
     },
     "minimatch": {
       "version": "3.0.4",
@@ -7766,6 +7743,15 @@
       "integrity": "sha512-eBpLEjI6tK4RKK44BbUBQu89lrNh+5WeX3wf2U6Uwo6RtRGAQ77qvKeuuQh3lVXHF1aPndVww9VcjqmLThIdtA==",
       "dev": true
     },
+    "mock-spawn": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmjs.org/mock-spawn/-/mock-spawn-0.2.6.tgz",
+      "integrity": "sha1-s5wVocBnUEMQFEFR8sHeNE0Dk38=",
+      "dev": true,
+      "requires": {
+        "through": "2.3.x"
+      }
+    },
     "moment": {
       "version": "2.24.0",
       "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
@@ -8309,7 +8295,6 @@
       "version": "5.1.4",
       "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz",
       "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==",
-      "optional": true,
       "requires": {
         "asn1.js": "^4.0.0",
         "browserify-aes": "^1.0.0",
@@ -8418,7 +8403,6 @@
       "version": "3.0.17",
       "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
       "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
-      "optional": true,
       "requires": {
         "create-hash": "^1.1.2",
         "create-hmac": "^1.1.4",
@@ -8621,8 +8605,7 @@
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
-      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
-      "optional": true
+      "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
     },
     "pseudomap": {
       "version": "1.0.2",
@@ -8689,7 +8672,6 @@
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
       "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
-      "optional": true,
       "requires": {
         "safe-buffer": "^5.1.0"
       }
@@ -9102,7 +9084,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
       "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
-      "optional": true,
       "requires": {
         "hash-base": "^3.0.0",
         "inherits": "^2.0.1"
@@ -9492,7 +9473,6 @@
       "version": "2.4.11",
       "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
       "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
-      "optional": true,
       "requires": {
         "inherits": "^2.0.1",
         "safe-buffer": "^5.0.1"
@@ -9688,8 +9668,7 @@
     "source-list-map": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
-      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==",
-      "optional": true
+      "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw=="
     },
     "source-map": {
       "version": "0.7.3",
@@ -10246,8 +10225,7 @@
     "tapable": {
       "version": "0.2.9",
       "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz",
-      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==",
-      "optional": true
+      "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A=="
     },
     "tar-stream": {
       "version": "1.6.2",
@@ -11097,7 +11075,6 @@
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",
       "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==",
-      "optional": true,
       "requires": {
         "source-list-map": "^2.0.0",
         "source-map": "~0.6.1"
@@ -11106,8 +11083,7 @@
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-          "optional": true
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
         }
       }
     },
diff --git a/package.json b/package.json
index 5def538..23dec2b 100644
--- a/package.json
+++ b/package.json
@@ -76,6 +76,7 @@
     "jest": "^24.8.0",
     "jest-cli": "^24.8.0",
     "mock-fs": "^4.10.0",
+    "mock-spawn": "^0.2.6",
     "serverless": "^1.44.1",
     "shx": "^0.3.2",
     "typescript": "^3.4.5"
diff --git a/src/config.ts b/src/config.ts
index 9cf1047..36414ee 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -9,6 +9,9 @@ export const configConstants = {
   functionAppDomain: ".azurewebsites.net",
   functionsAdminApiPath: "/admin/functions/",
   functionsApiPath: "/api/functions",
+  funcCoreTools: "func",
+  funcCoreToolsArgs: ["host", "start"],
+  funcConsoleColor: "blue",
   jsonContentType: "application/json",
   logInvocationsApiPath: "/azurejobs/api/functions/definitions/",
   logOutputApiPath: "/azurejobs/api/log/output/",
@@ -21,4 +24,4 @@ export const configConstants = {
   scmZipDeployApiPath: "/api/zipdeploy"
 };
 
-export default configConstants;
\ No newline at end of file
+export default configConstants;
diff --git a/src/models/serverless.ts b/src/models/serverless.ts
index c15adf1..240b6ca 100644
--- a/src/models/serverless.ts
+++ b/src/models/serverless.ts
@@ -94,3 +94,9 @@ export interface ServerlessCommandMap {
 export interface ServerlessAzureOptions extends Serverless.Options {
   resourceGroup?: string;
 }
+
+export interface ServerlessLogOptions {
+  underline?: boolean;
+  bold?: boolean;
+  color?: string;
+}
diff --git a/src/plugins/offline/azureOfflinePlugin.ts b/src/plugins/offline/azureOfflinePlugin.ts
index 2f5f879..9fd7cb0 100644
--- a/src/plugins/offline/azureOfflinePlugin.ts
+++ b/src/plugins/offline/azureOfflinePlugin.ts
@@ -15,6 +15,7 @@ export class AzureOfflinePlugin extends AzureBasePlugin {
       "before:offline:offline": this.azureOfflineBuild.bind(this),
       "offline:build:build": this.azureOfflineBuild.bind(this),
       "offline:offline": this.azureOfflineStart.bind(this),
+      "offline:start:start": this.azureOfflineStart.bind(this),
       "offline:cleanup:cleanup": this.azureOfflineCleanup.bind(this),
     };
 
@@ -25,6 +26,12 @@ export class AzureOfflinePlugin extends AzureBasePlugin {
           "offline",
         ],
         commands: {
+          start: {
+            usage: "Start Azure Function app - assumes offline build has already occurred",
+            lifecycleEvents: [
+              "start"
+            ]
+          },
           build: {
             usage: "Build necessary files for running Azure Function App offline",
             lifecycleEvents: [
@@ -43,14 +50,14 @@ export class AzureOfflinePlugin extends AzureBasePlugin {
   }
 
   private async azureOfflineBuild(){
-    this.offlineService.build();
+    await this.offlineService.build();
   }
 
   private async azureOfflineStart(){
-    this.offlineService.start();
+    await this.offlineService.start();
   }
 
   private async azureOfflineCleanup(){
-    this.offlineService.cleanup();
+    await this.offlineService.cleanup();
   }
 }
diff --git a/src/services/baseService.ts b/src/services/baseService.ts
index 7b21371..c5715f4 100644
--- a/src/services/baseService.ts
+++ b/src/services/baseService.ts
@@ -1,13 +1,18 @@
+import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth";
 import axios from "axios";
 import fs from "fs";
 import request from "request";
 import Serverless from "serverless";
-import { ServerlessAzureOptions, ServerlessAzureFunctionConfig } from "../models/serverless";
 import { StorageAccountResource } from "../armTemplates/resources/storageAccount";
 import { configConstants } from "../config";
-import { DeploymentConfig, ServerlessAzureConfig } from "../models/serverless";
+import {
+  DeploymentConfig,
+  ServerlessAzureConfig,
+  ServerlessAzureFunctionConfig,
+  ServerlessAzureOptions,
+  ServerlessLogOptions
+} from "../models/serverless";
 import { Guard } from "../shared/guard";
-import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth";
 import { Utils } from "../shared/utils";
 
 export abstract class BaseService {
@@ -174,8 +179,8 @@ export abstract class BaseService {
    * Log message to Serverless CLI
    * @param message Message to log
    */
-  protected log(message: string) {
-    this.serverless.cli.log(message);
+  protected log(message: string, options?: ServerlessLogOptions, entity?: string,) {
+    (this.serverless.cli.log as any)(message, entity, options);
   }
 
   /**
diff --git a/src/services/offlineService.ts b/src/services/offlineService.ts
index 35f62e3..40ac855 100644
--- a/src/services/offlineService.ts
+++ b/src/services/offlineService.ts
@@ -1,5 +1,7 @@
-import Serverless from "serverless";
+import { spawn } from "child_process";
 import fs from "fs";
+import Serverless from "serverless";
+import configConstants from "../config";
 import { BaseService } from "./baseService";
 import { PackageService } from "./packageService";
 
@@ -50,9 +52,70 @@ export class OfflineService extends BaseService {
     this.log("Finished cleaning up offline files");
   }
 
-  public start() {
-    this.log("Run 'npm start' or 'func host start' to run service locally");
-    this.log("Make sure you have Azure Functions Core Tools installed");
-    this.log("If not installed run 'npm i azure-functions-core-tools -g")
+  /**
+   * Spawn `func host start` from core func tools
+   */
+  public async start() {
+    await this.spawn(configConstants.funcCoreTools, configConstants.funcCoreToolsArgs);
+  }
+
+  /**
+   * Spawn a Node child process with predefined environment variables
+   * @param command CLI Command - NO ARGS
+   * @param args Array of arguments for CLI command
+   * @param env Additional environment variables to be set in addition to
+   * predefined variables in `serverless.yml`
+   */
+  private spawn(command: string, args?: string[], env?: any): Promise<void> {
+    env = {
+      ...this.serverless.service.provider["environment"],
+      ...env
+    }
+    this.log(`Spawning process '${command} ${args.join(" ")}'`);
+    return new Promise((resolve, reject) => {
+      const childProcess = spawn(command, args, {env});
+
+      childProcess.stdout.on("data", (data) => {
+        this.log(data, {
+          color: configConstants.funcConsoleColor,
+        }, command);
+      });
+
+      childProcess.stderr.on("data", (data) => {
+        this.log(data, {
+          color: "red",
+        }, command);
+      })
+
+      childProcess.on("message", (message) => {
+        this.log(message, {
+          color: configConstants.funcConsoleColor,
+        }, command);
+      });
+
+      childProcess.on("error", (err) => {
+        this.log(`${err}`, {
+          color: "red"
+        }, command);
+        reject(err);
+      });
+
+      childProcess.on("exit", (code) => {
+        this.log(`Exited with code: ${code}`, {
+          color: (code === 0) ? "green" : "red",
+        }, command);
+      });
+
+      childProcess.on("close", (code) => {
+        this.log(`Closed with code: ${code}`, {
+          color: (code === 0) ? "green" : "red",
+        }, command);
+        resolve();
+      });
+
+      childProcess.on("disconnect", () => {
+        this.log("Process disconnected");
+      });
+    });
   }
 }


Test patch:
diff --git a/src/plugins/func/azureFuncPlugin.test.ts b/src/plugins/func/azureFuncPlugin.test.ts
index 57db0e8..9869dbd 100644
--- a/src/plugins/func/azureFuncPlugin.test.ts
+++ b/src/plugins/func/azureFuncPlugin.test.ts
@@ -44,7 +44,11 @@ describe("Azure Func Plugin", () => {
       const plugin = new AzureFuncPlugin(sls, options);
       await invokeHook(plugin, "func:add:add");
 
-      expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to add")
+      expect(sls.cli.log).toBeCalledWith(
+        "Need to provide a name of function to add",
+        undefined,
+        undefined
+      )
     });
 
     it("returns with pre-existing function", async () => {
@@ -98,7 +102,11 @@ describe("Azure Func Plugin", () => {
       const options = MockFactory.createTestServerlessOptions();
       const plugin = new AzureFuncPlugin(sls, options);
       await invokeHook(plugin, "func:remove:remove");
-      expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to remove")
+      expect(sls.cli.log).toBeCalledWith(
+        "Need to provide a name of function to remove",
+        undefined,
+        undefined
+      )
     });
 
     it("returns with non-existing function", async () => {
@@ -107,7 +115,11 @@ describe("Azure Func Plugin", () => {
       options["name"] = "myNonExistingFunction";
       const plugin = new AzureFuncPlugin(sls, options);
       await invokeHook(plugin, "func:remove:remove");
-      expect(sls.cli.log).toBeCalledWith("Function myNonExistingFunction does not exist");
+      expect(sls.cli.log).toBeCalledWith(
+        "Function myNonExistingFunction does not exist",
+        undefined,
+        undefined
+      );
     });
 
     it("deletes directory and updates serverless.yml", async () => {
diff --git a/src/services/baseService.test.ts b/src/services/baseService.test.ts
index 1ecf667..d001cb8 100644
--- a/src/services/baseService.test.ts
+++ b/src/services/baseService.test.ts
@@ -1,14 +1,16 @@
+import fs from "fs";
+import mockFs from "mock-fs";
 import Serverless from "serverless";
+import { ServerlessAzureOptions } from "../models/serverless";
+import { Utils } from "../shared/utils";
+import { MockFactory } from "../test/mockFactory";
+import { BaseService } from "./baseService";
+
 jest.mock("axios", () => jest.fn());
 import axios from "axios";
-import mockFs from "mock-fs";
-import { MockFactory } from "../test/mockFactory";
+
 jest.mock("request", () => MockFactory.createTestMockRequestFactory());
 import request from "request";
-import fs from "fs";
-import { BaseService } from "./baseService";
-import { ServerlessAzureOptions } from "../models/serverless";
-import { Utils } from "../shared/utils";
 
 class MockService extends BaseService {
   public constructor(serverless: Serverless, options?: ServerlessAzureOptions) {
diff --git a/src/services/funcService.test.ts b/src/services/funcService.test.ts
index b42bb75..fd0d9d1 100644
--- a/src/services/funcService.test.ts
+++ b/src/services/funcService.test.ts
@@ -41,7 +41,7 @@ describe("Azure Func Service", () => {
       const service = createService(sls, options);
       await service.add();
 
-      expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to add")
+      expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to add", undefined, undefined);
     });
 
     it("returns with pre-existing function", async () => {
@@ -94,7 +94,11 @@ describe("Azure Func Service", () => {
       const options = MockFactory.createTestServerlessOptions();
       const service = createService(sls, options);
       await service.remove();
-      expect(sls.cli.log).toBeCalledWith("Need to provide a name of function to remove")
+      expect(sls.cli.log).toBeCalledWith(
+        "Need to provide a name of function to remove",
+        undefined,
+        undefined
+      )
     });
 
     it("returns with non-existing function", async () => {
@@ -103,7 +107,10 @@ describe("Azure Func Service", () => {
       options["name"] = "myNonExistingFunction";
       const service = createService(sls, options);
       await service.remove();
-      expect(sls.cli.log).toBeCalledWith("Function myNonExistingFunction does not exist");
+      expect(sls.cli.log).toBeCalledWith("Function myNonExistingFunction does not exist",
+        undefined,
+        undefined
+      );
     });
 
     it("deletes directory and updates serverless.yml", async () => {
diff --git a/src/services/offlineService.test.ts b/src/services/offlineService.test.ts
index 979dc0c..c8fc4f4 100644
--- a/src/services/offlineService.test.ts
+++ b/src/services/offlineService.test.ts
@@ -1,5 +1,6 @@
 import fs from "fs";
 import mockFs from "mock-fs";
+import mockSpawn from "mock-spawn";
 import path from "path";
 import Serverless from "serverless";
 import { MockFactory } from "../test/mockFactory";
@@ -7,6 +8,10 @@ import { OfflineService } from "./offlineService";
 
 describe("Offline Service", () => {
 
+  const mySpawn = mockSpawn();
+  require("child_process").spawn = mySpawn;
+  mySpawn.setDefault(mySpawn.simple(0, "Exit code"));
+
   function createService(sls?: Serverless): OfflineService {
     return new OfflineService(
       sls || MockFactory.createTestServerless(),
@@ -112,9 +117,10 @@ describe("Offline Service", () => {
     const sls = MockFactory.createTestServerless();
     const service = createService(sls);
     await service.start();
-    // Trivial test for now. In the future, this process
-    // may spawn the start process itself rather than telling
-    // the user how to do it.
-    expect(sls.cli.log).toBeCalledTimes(3);
+    const calls = mySpawn.calls;
+    expect(calls).toHaveLength(1);
+    const call = calls[0];
+    expect(call.command).toEqual("func");
+    expect(call.args).toEqual(["host", "start"]);
   });
 });
diff --git a/src/services/rollbackService.test.ts b/src/services/rollbackService.test.ts
index 547bbe2..ffdf0e9 100644
--- a/src/services/rollbackService.test.ts
+++ b/src/services/rollbackService.test.ts
@@ -74,7 +74,11 @@ describe("Rollback Service", () => {
     const options = {} as any;
     const service = createService(sls, options);
     await service.rollback();
-    expect(sls.cli.log).lastCalledWith(deploymentString);
+    expect(sls.cli.log).lastCalledWith(
+      deploymentString,
+      undefined,
+      undefined
+    );
   });
 
   it("should return early with invalid timestamp", async () => {