refactor: fetch target (simulator) with execa (#159)

diff --git a/lib/paramedic.js b/lib/paramedic.js
index d856ec6..e431da1 100644
--- a/lib/paramedic.js
+++ b/lib/paramedic.js
@@ -50,6 +50,10 @@
         this.targetObj = undefined;
         this.paramedicSauceLabs = null;
 
+        this.isBrowser = this.config.getPlatformId() === utilities.BROWSER;
+        this.isWindows = this.config.getPlatformId() === utilities.WINDOWS;
+        this.isIos = this.config.getPlatformId() === utilities.IOS;
+
         exec.setVerboseLevel(config.isVerbose());
     }
 
@@ -68,7 +72,7 @@
             .then(() => {
                 if (this.config.runMainTests()) {
                 // start server
-                    const noListener = (this.config.getPlatformId() === utilities.BROWSER) && this.config.shouldUseSauce();
+                    const noListener = (this.isBrowser) && this.config.shouldUseSauce();
                     return Server.startServer(this.config.getPorts(), this.config.getExternalServerUrl(), this.config.getUseTunnel(), noListener);
                 }
             })
@@ -143,7 +147,7 @@
 
     setPermissions () {
         const applicationsToGrantPermission = ['kTCCServiceAddressBook'];
-        if (this.config.getPlatformId() === utilities.IOS) {
+        if (this.isIos) {
             logger.info('cordova-paramedic: Setting required permissions.');
             const tccDb = this.config.getTccDb();
             if (tccDb) {
@@ -377,42 +381,37 @@
     }
 
     getCommandForStartingTests () {
-        let cmd = this.config.getCli() + ' ' + this.config.getAction() + ' ' + this.config.getPlatformId() + utilities.PARAMEDIC_COMMON_CLI_ARGS;
+        let cmd = [
+            this.config.getCli(),
+            this.config.getAction(),
+            this.config.getPlatformId()
+        ]
+            .concat(utilities.PARAMEDIC_COMMON_ARGS)
+            .concat([this.config.getArgs()]);
 
-        const addConfigArgs = (cmd) => {
-            if (this.config.getArgs()) {
-                cmd += ' ' + this.config.getArgs();
-            }
-
-            return cmd;
-        };
-
-        if (this.config.getPlatformId() === utilities.BROWSER) {
-            return addConfigArgs(cmd);
-        }
-
-        const paramedicTargetChooser = new ParamedicTargetChooser(this.tempFolder.name, this.config);
-
-        if (this.config.getAction() === 'build' || (this.config.getPlatformId() === utilities.WINDOWS && this.config.getArgs().indexOf('appx=8.1-phone') < 0)) {
+        if (this.isBrowser) {
+            return cmd.join(' ');
+        } else if (this.config.getAction() === 'build' || (this.isWindows && this.config.getArgs().indexOf('appx=8.1-phone') < 0)) {
             // The app is to be run as a store app or just build. So no need to choose a target.
-            return Q(addConfigArgs(cmd));
+            return Q(cmd.join(' '));
         }
 
         // For now we always trying to run test app on emulator
-        return Q().then(() => paramedicTargetChooser.chooseTarget(/* useEmulator= */true, /* preferredTarget= */this.config.getTarget()))
-            .then((targetObj) => {
-                this.targetObj = targetObj;
-                cmd += ` --target "${this.targetObj.target}"`;
+        return (new ParamedicTargetChooser(this.tempFolder.name, this.config)).chooseTarget(
+            true, // useEmulator
+            this.config.getTarget() // preferredTarget
+        ).then(targetObj => {
+            this.targetObj = targetObj;
+
+            return cmd
+                .concat(['--target', `"${this.targetObj.target}"`])
 
                 // CB-11472 In case of iOS provide additional '--emulator' flag, otherwise
                 // 'cordova run ios --target' would hang waiting for device with name
                 // as specified in 'target' in case if any device is physically connected
-                if (this.config.getPlatformId() === utilities.IOS) {
-                    cmd += ' --emulator';
-                }
-
-                return addConfigArgs(cmd);
-            });
+                .concat(this.isIos ? ['--emulator'] : [])
+                .join(' ');
+        });
     }
 
     shouldWaitForTestResult () {
diff --git a/lib/utils/utilities.js b/lib/utils/utilities.js
index 78e1d45..f42fd4c 100644
--- a/lib/utils/utilities.js
+++ b/lib/utils/utilities.js
@@ -26,6 +26,7 @@
 const logger = require('cordova-common').CordovaLogger.get();
 const kill = require('tree-kill');
 const exec = require('./execWrapper').exec;
+const execa = require('execa');
 
 const HEADING_LINE_PATTERN = /List of devices/m;
 const DEVICE_ROW_PATTERN = /(emulator|device|host)/m;
@@ -61,23 +62,31 @@
 }
 
 function getSimulatorModelId (cli, target) {
-    target = target || '^iPhone';
+    target = new RegExp(target || '^iPhone');
 
-    // Command to fetch for a list of simulators by target. defaults to (^iPhone)
-    const cmd = `${cli} run --list --emulator${module.exports.PARAMEDIC_COMMON_CLI_ARGS} | grep "${target}" | tail -n1`;
+    const args = [
+        'run',
+        '--list',
+        '--emulator'
+    ].concat(module.exports.PARAMEDIC_COMMON_ARGS);
 
+    // Fetches all known simulators/emulators.
     logger.info('running:');
-    logger.info('    ' + cmd);
+    logger.info(`    ${cli} ${args.join(' ')}`);
 
-    // Results of the fetching command.
-    const results = exec(cmd);
+    const result = execa.sync(cli, args);
 
-    if (results.code > 0) {
+    if (result.exitCode > 0) {
         logger.error('Failed to find simulator we deployed to');
         return;
     }
 
-    return results.stdout;
+    // Return the individual target that is filtered from the known simulators/emulators based on provided target name. (default: ^iPhone)
+    return result.stdout
+        .split('\n')
+        .filter(i => i.match(target))
+        .pop()
+        .trim();
 }
 
 function getSimulatorId (findSimResult) {
@@ -191,6 +200,7 @@
     BROWSER: 'browser',
     PARAMEDIC_DEFAULT_APP_NAME: 'io.cordova.hellocordova',
     PARAMEDIC_COMMON_CLI_ARGS: ' --no-telemetry --no-update-notifier',
+    PARAMEDIC_COMMON_ARGS: ['--no-telemetry', '--no-update-notifier'],
     PARAMEDIC_PLUGIN_ADD_ARGS: '',
     PARAMEDIC_PLATFORM_ADD_ARGS: '',
     SAUCE_USER_ENV_VAR: 'SAUCE_USERNAME',
diff --git a/package-lock.json b/package-lock.json
index 38aa4c4..30de9a4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1142,6 +1142,21 @@
       "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
       "dev": true
     },
+    "execa": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-2.0.1.tgz",
+      "integrity": "sha512-pHGXlV7S7ilDda3eaCTcr6zmFTMA3wJo7j+RtNg0uH9sbAasJfVug5RkYOTBLj5g4MOqlsaPUn3HKa/UfTDw8w==",
+      "requires": {
+        "cross-spawn": "^6.0.5",
+        "get-stream": "^5.0.0",
+        "is-stream": "^2.0.0",
+        "merge-stream": "^2.0.0",
+        "npm-run-path": "^3.0.0",
+        "p-finally": "^2.0.0",
+        "signal-exit": "^3.0.2",
+        "strip-final-newline": "^2.0.0"
+      }
+    },
     "expect-telnet": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/expect-telnet/-/expect-telnet-1.0.0.tgz",
@@ -1346,6 +1361,14 @@
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
       "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w=="
     },
+    "get-stream": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
+      "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
+      "requires": {
+        "pump": "^3.0.0"
+      }
+    },
     "getpass": {
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
@@ -1612,6 +1635,11 @@
         "has": "^1.0.1"
       }
     },
+    "is-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
+      "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
+    },
     "is-symbol": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
@@ -1826,6 +1854,11 @@
         "path-exists": "^3.0.0"
       }
     },
+    "merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
+    },
     "mime-db": {
       "version": "1.37.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
@@ -1919,6 +1952,21 @@
         "remove-trailing-separator": "^1.0.1"
       }
     },
+    "npm-run-path": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz",
+      "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==",
+      "requires": {
+        "path-key": "^3.0.0"
+      },
+      "dependencies": {
+        "path-key": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.0.tgz",
+          "integrity": "sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg=="
+        }
+      }
+    },
     "number-is-nan": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
@@ -2016,6 +2064,11 @@
       "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
       "dev": true
     },
+    "p-finally": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz",
+      "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw=="
+    },
     "p-limit": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
@@ -2173,6 +2226,15 @@
       "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
       "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ=="
     },
+    "pump": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+      "requires": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
     "punycode": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -2418,8 +2480,7 @@
     "signal-exit": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
-      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
-      "dev": true
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
     },
     "slice-ansi": {
       "version": "2.1.0",
@@ -2613,6 +2674,11 @@
       "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
       "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM="
     },
+    "strip-final-newline": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
+    },
     "strip-json-comments": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
diff --git a/package.json b/package.json
index ddcb279..bd615b1 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
   "author": "Apache Software Foundation",
   "dependencies": {
     "cordova-common": "^3.1.0",
+    "execa": "^2.0.1",
     "expect-telnet": "^1.0.0",
     "jasmine": "^3.4.0",
     "jasmine-reporters": "^2.3.2",