Index: frontend/node_modules/shell-quote/.eslintrc
===================================================================
--- frontend/node_modules/shell-quote/.eslintrc	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/.eslintrc	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,30 @@
+{
+	"root": true,
+
+	"extends": "@ljharb",
+
+	"rules": {
+		"array-bracket-newline": 0,
+		"complexity": 0,
+		"eqeqeq": 1,
+		"func-style": [2, "declaration"],
+		"max-depth": 0,
+		"max-lines-per-function": 0,
+		"max-statements": 0,
+		"multiline-comment-style": 0,
+		"no-negated-condition": 1,
+		"no-param-reassign": 1,
+    "no-lonely-if": 1,
+		"no-shadow": 1,
+		"no-template-curly-in-string": 0,
+	},
+
+	"overrides": [
+		{
+			"files": "example/**",
+			"rules": {
+				"no-console": 0,
+			},
+		},
+	],
+}
Index: frontend/node_modules/shell-quote/.github/FUNDING.yml
===================================================================
--- frontend/node_modules/shell-quote/.github/FUNDING.yml	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/.github/FUNDING.yml	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: [ljharb]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: npm/shell-quote
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
Index: frontend/node_modules/shell-quote/.nycrc
===================================================================
--- frontend/node_modules/shell-quote/.nycrc	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/.nycrc	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,14 @@
+{
+	"all": true,
+	"check-coverage": false,
+	"reporter": ["text-summary", "text", "html", "json"],
+	"lines": 86,
+	"statements": 85.93,
+	"functions": 82.43,
+	"branches": 76.06,
+	"exclude": [
+		"coverage",
+		"example",
+		"test"
+	]
+}
Index: frontend/node_modules/shell-quote/LICENSE
===================================================================
--- frontend/node_modules/shell-quote/LICENSE	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/LICENSE	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,24 @@
+The MIT License
+
+Copyright (c) 2013 James Halliday (mail@substack.net)
+
+Permission is hereby granted, free of charge, 
+to any person obtaining a copy of this software and 
+associated documentation files (the "Software"), to 
+deal in the Software without restriction, including 
+without limitation the rights to use, copy, modify, 
+merge, publish, distribute, sublicense, and/or sell 
+copies of the Software, and to permit persons to whom 
+the Software is furnished to do so, 
+subject to the following conditions:
+
+The above copyright notice and this permission notice 
+shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Index: frontend/node_modules/shell-quote/README.md
===================================================================
--- frontend/node_modules/shell-quote/README.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/README.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,161 @@
+# shell-quote <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
+
+[![github actions][actions-image]][actions-url]
+[![coverage][codecov-image]][codecov-url]
+[![License][license-image]][license-url]
+[![Downloads][downloads-image]][downloads-url]
+
+[![npm badge][npm-badge-png]][package-url]
+
+Parse and quote shell commands.
+
+# example
+
+## quote
+
+``` js
+var quote = require('shell-quote/quote');
+var s = quote([ 'a', 'b c d', '$f', '"g"' ]);
+console.log(s);
+```
+
+output
+
+```
+a 'b c d' \$f '"g"'
+```
+
+## parse
+
+``` js
+var parse = require('shell-quote/parse');
+var xs = parse('a "b c" \\$def \'it\\\'s great\'');
+console.dir(xs);
+```
+
+output
+
+```
+[ 'a', 'b c', '\\$def', 'it\'s great' ]
+```
+
+## parse with an environment variable
+
+``` js
+var parse = require('shell-quote/parse');
+var xs = parse('beep --boop="$PWD"', { PWD: '/home/robot' });
+console.dir(xs);
+```
+
+output
+
+```
+[ 'beep', '--boop=/home/robot' ]
+```
+
+## parse with custom escape character
+
+``` js
+var parse = require('shell-quote/parse');
+var xs = parse('beep ^--boop="$PWD"', { PWD: '/home/robot' }, { escape: '^' });
+console.dir(xs);
+```
+
+output
+
+```
+[ 'beep --boop=/home/robot' ]
+```
+
+## parsing shell operators
+
+``` js
+var parse = require('shell-quote/parse');
+var xs = parse('beep || boop > /byte');
+console.dir(xs);
+```
+
+output:
+
+```
+[ 'beep', { op: '||' }, 'boop', { op: '>' }, '/byte' ]
+```
+
+## parsing shell comment
+
+``` js
+var parse = require('shell-quote/parse');
+var xs = parse('beep > boop # > kaboom');
+console.dir(xs);
+```
+
+output:
+
+```
+[ 'beep', { op: '>' }, 'boop', { comment: '> kaboom' } ]
+```
+
+# methods
+
+``` js
+var quote = require('shell-quote/quote');
+var parse = require('shell-quote/parse');
+```
+
+## quote(args)
+
+Return a quoted string for the array `args` suitable for using in shell
+commands.
+
+## parse(cmd, env={})
+
+Return an array of arguments from the quoted string `cmd`.
+
+Interpolate embedded bash-style `$VARNAME` and `${VARNAME}` variables with
+the `env` object which like bash will replace undefined variables with `""`.
+
+`env` is usually an object but it can also be a function to perform lookups.
+When `env(key)` returns a string, its result will be output just like `env[key]`
+would. When `env(key)` returns an object, it will be inserted into the result
+array like the operator objects.
+
+When a bash operator is encountered, the element in the array with be an object
+with an `"op"` key set to the operator string. For example:
+
+```
+'beep || boop > /byte'
+```
+
+parses as:
+
+```
+[ 'beep', { op: '||' }, 'boop', { op: '>' }, '/byte' ]
+```
+
+# install
+
+With [npm](http://npmjs.org) do:
+
+```
+npm install shell-quote
+```
+
+# license
+
+MIT
+
+[package-url]: https://npmjs.org/package/shell-quote
+[npm-version-svg]: https://versionbadg.es/ljharb/shell-quote.svg
+[deps-svg]: https://david-dm.org/ljharb/shell-quote.svg
+[deps-url]: https://david-dm.org/ljharb/shell-quote
+[dev-deps-svg]: https://david-dm.org/ljharb/shell-quote/dev-status.svg
+[dev-deps-url]: https://david-dm.org/ljharb/shell-quote#info=devDependencies
+[npm-badge-png]: https://nodei.co/npm/shell-quote.png?downloads=true&stars=true
+[license-image]: https://img.shields.io/npm/l/shell-quote.svg
+[license-url]: LICENSE
+[downloads-image]: https://img.shields.io/npm/dm/shell-quote.svg
+[downloads-url]: https://npm-stat.com/charts.html?package=shell-quote
+[codecov-image]: https://codecov.io/gh/ljharb/shell-quote/branch/main/graphs/badge.svg
+[codecov-url]: https://app.codecov.io/gh/ljharb/shell-quote/
+[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/shell-quote
+[actions-url]: https://github.com/ljharb/shell-quote/actions
Index: frontend/node_modules/shell-quote/index.js
===================================================================
--- frontend/node_modules/shell-quote/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,4 @@
+'use strict';
+
+exports.quote = require('./quote');
+exports.parse = require('./parse');
Index: frontend/node_modules/shell-quote/package.json
===================================================================
--- frontend/node_modules/shell-quote/package.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/package.json	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,72 @@
+{
+	"name": "shell-quote",
+	"description": "quote and parse shell commands",
+	"version": "1.8.3",
+	"author": {
+		"name": "James Halliday",
+		"email": "mail@substack.net",
+		"url": "http://substack.net"
+	},
+	"funding": {
+		"url": "https://github.com/sponsors/ljharb"
+	},
+	"bugs": "https://github.com/ljharb/shell-quote/issues",
+	"devDependencies": {
+		"@ljharb/eslint-config": "^21.1.1",
+		"auto-changelog": "^2.5.0",
+		"encoding": "^0.1.13",
+		"eslint": "=8.8.0",
+		"evalmd": "^0.0.19",
+		"in-publish": "^2.0.1",
+		"jackspeak": "=2.1.1",
+		"npmignore": "^0.3.1",
+		"nyc": "^10.3.2",
+		"safe-publish-latest": "^2.0.0",
+		"tape": "^5.9.0"
+	},
+	"homepage": "https://github.com/ljharb/shell-quote",
+	"keywords": [
+		"command",
+		"parse",
+		"quote",
+		"shell"
+	],
+	"license": "MIT",
+	"main": "index.js",
+	"repository": {
+		"type": "git",
+		"url": "http://github.com/ljharb/shell-quote.git"
+	},
+	"scripts": {
+		"prepack": "npmignore --auto --commentLines=autogenerated",
+		"prepublish": "not-in-publish || npm run prepublishOnly",
+		"prepublishOnly": "safe-publish-latest",
+		"prelint": "evalmd README.md",
+		"lint": "eslint --ext=js,mjs .",
+		"pretest": "npm run lint",
+		"tests-only": "nyc tape 'test/**/*.js'",
+		"test": "npm run tests-only",
+		"posttest": "npx npm@'>=10.2' audit --production",
+		"version": "auto-changelog && git add CHANGELOG.md",
+		"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
+	},
+	"auto-changelog": {
+		"output": "CHANGELOG.md",
+		"template": "keepachangelog",
+		"unreleased": false,
+		"commitLimit": false,
+		"backfillLimit": false,
+		"hideCredit": true,
+		"startingVersion": "1.7.4"
+	},
+	"publishConfig": {
+		"ignore": [
+			".github/workflows",
+			"example",
+			"CHANGELOG.md"
+		]
+	},
+	"engines": {
+		"node": ">= 0.4"
+	}
+}
Index: frontend/node_modules/shell-quote/parse.js
===================================================================
--- frontend/node_modules/shell-quote/parse.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/parse.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,226 @@
+'use strict';
+
+// '<(' is process substitution operator and
+// can be parsed the same as control operator
+var CONTROL = '(?:' + [
+	'\\|\\|',
+	'\\&\\&',
+	';;',
+	'\\|\\&',
+	'\\<\\(',
+	'\\<\\<\\<',
+	'>>',
+	'>\\&',
+	'<\\&',
+	'[&;()|<>]'
+].join('|') + ')';
+var controlRE = new RegExp('^' + CONTROL + '$');
+var META = '|&;()<> \\t';
+var SINGLE_QUOTE = '"((\\\\"|[^"])*?)"';
+var DOUBLE_QUOTE = '\'((\\\\\'|[^\'])*?)\'';
+var hash = /^#$/;
+
+var SQ = "'";
+var DQ = '"';
+var DS = '$';
+
+var TOKEN = '';
+var mult = 0x100000000; // Math.pow(16, 8);
+for (var i = 0; i < 4; i++) {
+	TOKEN += (mult * Math.random()).toString(16);
+}
+var startsWithToken = new RegExp('^' + TOKEN);
+
+function matchAll(s, r) {
+	var origIndex = r.lastIndex;
+
+	var matches = [];
+	var matchObj;
+
+	while ((matchObj = r.exec(s))) {
+		matches.push(matchObj);
+		if (r.lastIndex === matchObj.index) {
+			r.lastIndex += 1;
+		}
+	}
+
+	r.lastIndex = origIndex;
+
+	return matches;
+}
+
+function getVar(env, pre, key) {
+	var r = typeof env === 'function' ? env(key) : env[key];
+	if (typeof r === 'undefined' && key != '') {
+		r = '';
+	} else if (typeof r === 'undefined') {
+		r = '$';
+	}
+
+	if (typeof r === 'object') {
+		return pre + TOKEN + JSON.stringify(r) + TOKEN;
+	}
+	return pre + r;
+}
+
+function parseInternal(string, env, opts) {
+	if (!opts) {
+		opts = {};
+	}
+	var BS = opts.escape || '\\';
+	var BAREWORD = '(\\' + BS + '[\'"' + META + ']|[^\\s\'"' + META + '])+';
+
+	var chunker = new RegExp([
+		'(' + CONTROL + ')', // control chars
+		'(' + BAREWORD + '|' + SINGLE_QUOTE + '|' + DOUBLE_QUOTE + ')+'
+	].join('|'), 'g');
+
+	var matches = matchAll(string, chunker);
+
+	if (matches.length === 0) {
+		return [];
+	}
+	if (!env) {
+		env = {};
+	}
+
+	var commented = false;
+
+	return matches.map(function (match) {
+		var s = match[0];
+		if (!s || commented) {
+			return void undefined;
+		}
+		if (controlRE.test(s)) {
+			return { op: s };
+		}
+
+		// Hand-written scanner/parser for Bash quoting rules:
+		//
+		// 1. inside single quotes, all characters are printed literally.
+		// 2. inside double quotes, all characters are printed literally
+		//    except variables prefixed by '$' and backslashes followed by
+		//    either a double quote or another backslash.
+		// 3. outside of any quotes, backslashes are treated as escape
+		//    characters and not printed (unless they are themselves escaped)
+		// 4. quote context can switch mid-token if there is no whitespace
+		//     between the two quote contexts (e.g. all'one'"token" parses as
+		//     "allonetoken")
+		var quote = false;
+		var esc = false;
+		var out = '';
+		var isGlob = false;
+		var i;
+
+		function parseEnvVar() {
+			i += 1;
+			var varend;
+			var varname;
+			var char = s.charAt(i);
+
+			if (char === '{') {
+				i += 1;
+				if (s.charAt(i) === '}') {
+					throw new Error('Bad substitution: ' + s.slice(i - 2, i + 1));
+				}
+				varend = s.indexOf('}', i);
+				if (varend < 0) {
+					throw new Error('Bad substitution: ' + s.slice(i));
+				}
+				varname = s.slice(i, varend);
+				i = varend;
+			} else if ((/[*@#?$!_-]/).test(char)) {
+				varname = char;
+				i += 1;
+			} else {
+				var slicedFromI = s.slice(i);
+				varend = slicedFromI.match(/[^\w\d_]/);
+				if (!varend) {
+					varname = slicedFromI;
+					i = s.length;
+				} else {
+					varname = slicedFromI.slice(0, varend.index);
+					i += varend.index - 1;
+				}
+			}
+			return getVar(env, '', varname);
+		}
+
+		for (i = 0; i < s.length; i++) {
+			var c = s.charAt(i);
+			isGlob = isGlob || (!quote && (c === '*' || c === '?'));
+			if (esc) {
+				out += c;
+				esc = false;
+			} else if (quote) {
+				if (c === quote) {
+					quote = false;
+				} else if (quote == SQ) {
+					out += c;
+				} else { // Double quote
+					if (c === BS) {
+						i += 1;
+						c = s.charAt(i);
+						if (c === DQ || c === BS || c === DS) {
+							out += c;
+						} else {
+							out += BS + c;
+						}
+					} else if (c === DS) {
+						out += parseEnvVar();
+					} else {
+						out += c;
+					}
+				}
+			} else if (c === DQ || c === SQ) {
+				quote = c;
+			} else if (controlRE.test(c)) {
+				return { op: s };
+			} else if (hash.test(c)) {
+				commented = true;
+				var commentObj = { comment: string.slice(match.index + i + 1) };
+				if (out.length) {
+					return [out, commentObj];
+				}
+				return [commentObj];
+			} else if (c === BS) {
+				esc = true;
+			} else if (c === DS) {
+				out += parseEnvVar();
+			} else {
+				out += c;
+			}
+		}
+
+		if (isGlob) {
+			return { op: 'glob', pattern: out };
+		}
+
+		return out;
+	}).reduce(function (prev, arg) { // finalize parsed arguments
+		// TODO: replace this whole reduce with a concat
+		return typeof arg === 'undefined' ? prev : prev.concat(arg);
+	}, []);
+}
+
+module.exports = function parse(s, env, opts) {
+	var mapped = parseInternal(s, env, opts);
+	if (typeof env !== 'function') {
+		return mapped;
+	}
+	return mapped.reduce(function (acc, s) {
+		if (typeof s === 'object') {
+			return acc.concat(s);
+		}
+		var xs = s.split(RegExp('(' + TOKEN + '.*?' + TOKEN + ')', 'g'));
+		if (xs.length === 1) {
+			return acc.concat(xs[0]);
+		}
+		return acc.concat(xs.filter(Boolean).map(function (x) {
+			if (startsWithToken.test(x)) {
+				return JSON.parse(x.split(TOKEN)[1]);
+			}
+			return x;
+		}));
+	}, []);
+};
Index: frontend/node_modules/shell-quote/print.py
===================================================================
--- frontend/node_modules/shell-quote/print.py	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/print.py	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,3 @@
+#!/usr/bin/env python3
+import sys
+print(sys.argv[1])
Index: frontend/node_modules/shell-quote/quote.js
===================================================================
--- frontend/node_modules/shell-quote/quote.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/quote.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,19 @@
+'use strict';
+
+module.exports = function quote(xs) {
+	return xs.map(function (s) {
+		if (s === '') {
+			return '\'\'';
+		}
+		if (s && typeof s === 'object') {
+			return s.op.replace(/(.)/g, '\\$1');
+		}
+		if ((/["\s\\]/).test(s) && !(/'/).test(s)) {
+			return "'" + s.replace(/(['])/g, '\\$1') + "'";
+		}
+		if ((/["'\s]/).test(s)) {
+			return '"' + s.replace(/(["\\$`!])/g, '\\$1') + '"';
+		}
+		return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, '$1\\$2');
+	}).join(' ');
+};
Index: frontend/node_modules/shell-quote/security.md
===================================================================
--- frontend/node_modules/shell-quote/security.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/security.md	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,11 @@
+# Security Policy
+
+## Supported Versions
+
+Only the latest major version is supported at any given time.
+
+## Reporting a Vulnerability
+
+To report a security vulnerability, please use the
+[Tidelift security contact](https://tidelift.com/security).
+Tidelift will coordinate the fix and disclosure.
Index: frontend/node_modules/shell-quote/test/comment.js
===================================================================
--- frontend/node_modules/shell-quote/test/comment.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/test/comment.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,16 @@
+'use strict';
+
+var test = require('tape');
+var parse = require('../').parse;
+
+test('comment', function (t) {
+	t.same(parse('beep#boop'), ['beep', { comment: 'boop' }]);
+	t.same(parse('beep #boop'), ['beep', { comment: 'boop' }]);
+	t.same(parse('beep # boop'), ['beep', { comment: ' boop' }]);
+	t.same(parse('beep # > boop'), ['beep', { comment: ' > boop' }]);
+	t.same(parse('beep # "> boop"'), ['beep', { comment: ' "> boop"' }]);
+	t.same(parse('beep "#"'), ['beep', '#']);
+	t.same(parse('beep #"#"#'), ['beep', { comment: '"#"#' }]);
+	t.same(parse('beep > boop # > foo'), ['beep', { op: '>' }, 'boop', { comment: ' > foo' }]);
+	t.end();
+});
Index: frontend/node_modules/shell-quote/test/env.js
===================================================================
--- frontend/node_modules/shell-quote/test/env.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/test/env.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,52 @@
+'use strict';
+
+var test = require('tape');
+var parse = require('../').parse;
+
+test('expand environment variables', function (t) {
+	t.same(parse('a $XYZ c', { XYZ: 'b' }), ['a', 'b', 'c']);
+	t.same(parse('a${XYZ}c', { XYZ: 'b' }), ['abc']);
+	t.same(parse('a${XYZ}c $XYZ', { XYZ: 'b' }), ['abc', 'b']);
+	t.same(parse('"-$X-$Y-"', { X: 'a', Y: 'b' }), ['-a-b-']);
+	t.same(parse("'-$X-$Y-'", { X: 'a', Y: 'b' }), ['-$X-$Y-']);
+	t.same(parse('qrs"$zzz"wxy', { zzz: 'tuv' }), ['qrstuvwxy']);
+	t.same(parse("qrs'$zzz'wxy", { zzz: 'tuv' }), ['qrs$zzzwxy']);
+	t.same(parse('qrs${zzz}wxy'), ['qrswxy']);
+	t.same(parse('qrs$wxy $'), ['qrs', '$']);
+	t.same(parse('grep "xy$"'), ['grep', 'xy$']);
+	t.same(parse('ab$x', { x: 'c' }), ['abc']);
+	t.same(parse('ab\\$x', { x: 'c' }), ['ab$x']);
+	t.same(parse('ab${x}def', { x: 'c' }), ['abcdef']);
+	t.same(parse('ab\\${x}def', { x: 'c' }), ['ab${x}def']);
+	t.same(parse('"ab\\${x}def"', { x: 'c' }), ['ab${x}def']);
+
+	t.end();
+});
+
+test('expand environment variables within here-strings', function (t) {
+	t.same(parse('a <<< $x', { x: 'Joe' }), ['a', { op: '<<<' }, 'Joe']);
+	t.same(parse('a <<< ${x}', { x: 'Joe' }), ['a', { op: '<<<' }, 'Joe']);
+	t.same(parse('a <<< "$x"', { x: 'Joe' }), ['a', { op: '<<<' }, 'Joe']);
+	t.same(parse('a <<< "${x}"', { x: 'Joe' }), ['a', { op: '<<<' }, 'Joe']);
+
+	t.end();
+});
+
+test('environment variables with metacharacters', function (t) {
+	t.same(parse('a $XYZ c', { XYZ: '"b"' }), ['a', '"b"', 'c']);
+	t.same(parse('a $XYZ c', { XYZ: '$X', X: 5 }), ['a', '$X', 'c']);
+	t.same(parse('a"$XYZ"c', { XYZ: "'xyz'" }), ["a'xyz'c"]);
+
+	t.end();
+});
+
+test('special shell parameters', function (t) {
+	var chars = '*@#?-$!0_'.split('');
+	t.plan(chars.length);
+
+	chars.forEach(function (c) {
+		var env = {};
+		env[c] = 'xxx';
+		t.same(parse('a $' + c + ' c', env), ['a', 'xxx', 'c']);
+	});
+});
Index: frontend/node_modules/shell-quote/test/env_fn.js
===================================================================
--- frontend/node_modules/shell-quote/test/env_fn.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/test/env_fn.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,21 @@
+'use strict';
+
+var test = require('tape');
+var parse = require('../').parse;
+
+function getEnv() {
+	return 'xxx';
+}
+
+function getEnvObj() {
+	return { op: '@@' };
+}
+
+test('functional env expansion', function (t) {
+	t.plan(4);
+
+	t.same(parse('a $XYZ c', getEnv), ['a', 'xxx', 'c']);
+	t.same(parse('a $XYZ c', getEnvObj), ['a', { op: '@@' }, 'c']);
+	t.same(parse('a${XYZ}c', getEnvObj), ['a', { op: '@@' }, 'c']);
+	t.same(parse('"a $XYZ c"', getEnvObj), ['a ', { op: '@@' }, ' c']);
+});
Index: frontend/node_modules/shell-quote/test/op.js
===================================================================
--- frontend/node_modules/shell-quote/test/op.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/test/op.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,102 @@
+'use strict';
+
+var test = require('tape');
+var parse = require('../').parse;
+
+test('single operators', function (t) {
+	t.same(parse('beep | boop'), ['beep', { op: '|' }, 'boop']);
+	t.same(parse('beep|boop'), ['beep', { op: '|' }, 'boop']);
+	t.same(parse('beep \\| boop'), ['beep', '|', 'boop']);
+	t.same(parse('beep "|boop"'), ['beep', '|boop']);
+
+	t.same(parse('echo zing &'), ['echo', 'zing', { op: '&' }]);
+	t.same(parse('echo zing&'), ['echo', 'zing', { op: '&' }]);
+	t.same(parse('echo zing\\&'), ['echo', 'zing&']);
+	t.same(parse('echo "zing\\&"'), ['echo', 'zing\\&']);
+
+	t.same(parse('beep;boop'), ['beep', { op: ';' }, 'boop']);
+	t.same(parse('(beep;boop)'), [
+		{ op: '(' }, 'beep', { op: ';' }, 'boop', { op: ')' }
+	]);
+
+	t.same(parse('beep>boop'), ['beep', { op: '>' }, 'boop']);
+	t.same(parse('beep 2>boop'), ['beep', '2', { op: '>' }, 'boop']);
+	t.same(parse('beep<boop'), ['beep', { op: '<' }, 'boop']);
+
+	t.end();
+});
+
+test('double operators', function (t) {
+	t.same(parse('beep || boop'), ['beep', { op: '||' }, 'boop']);
+	t.same(parse('beep||boop'), ['beep', { op: '||' }, 'boop']);
+	t.same(parse('beep ||boop'), ['beep', { op: '||' }, 'boop']);
+	t.same(parse('beep|| boop'), ['beep', { op: '||' }, 'boop']);
+	t.same(parse('beep  ||   boop'), ['beep', { op: '||' }, 'boop']);
+
+	t.same(parse('beep && boop'), ['beep', { op: '&&' }, 'boop']);
+	t.same(
+		parse('beep && boop || byte'),
+		['beep', { op: '&&' }, 'boop', { op: '||' }, 'byte']
+	);
+	t.same(
+		parse('beep&&boop||byte'),
+		['beep', { op: '&&' }, 'boop', { op: '||' }, 'byte']
+	);
+	t.same(
+		parse('beep\\&\\&boop||byte'),
+		['beep&&boop', { op: '||' }, 'byte']
+	);
+	t.same(
+		parse('beep\\&&boop||byte'),
+		['beep&', { op: '&' }, 'boop', { op: '||' }, 'byte']
+	);
+	t.same(
+		parse('beep;;boop|&byte>>blip'),
+		['beep', { op: ';;' }, 'boop', { op: '|&' }, 'byte', { op: '>>' }, 'blip']
+	);
+
+	t.same(parse('beep 2>&1'), ['beep', '2', { op: '>&' }, '1']);
+
+	t.same(
+		parse('beep<(boop)'),
+		['beep', { op: '<(' }, 'boop', { op: ')' }]
+	);
+	t.same(
+		parse('beep<<(boop)'),
+		['beep', { op: '<' }, { op: '<(' }, 'boop', { op: ')' }]
+	);
+
+	t.end();
+});
+
+test('duplicating input file descriptors', function (t) {
+	// duplicating stdout to file descriptor 3
+	t.same(parse('beep 3<&1'), ['beep', '3', { op: '<&' }, '1']);
+
+	// duplicating stdout to file descriptor 0, i.e. stdin
+	t.same(parse('beep <&1'), ['beep', { op: '<&' }, '1']);
+
+	// closes stdin
+	t.same(parse('beep <&-'), ['beep', { op: '<&' }, '-']);
+
+	t.end();
+});
+
+test('here strings', function (t) {
+	t.same(parse('cat <<< "hello world"'), ['cat', { op: '<<<' }, 'hello world']);
+	t.same(parse('cat <<< hello'), ['cat', { op: '<<<' }, 'hello']);
+	t.same(parse('cat<<<hello'), ['cat', { op: '<<<' }, 'hello']);
+	t.same(parse('cat<<<"hello world"'), ['cat', { op: '<<<' }, 'hello world']);
+
+	t.end();
+});
+
+test('glob patterns', function (t) {
+	t.same(
+		parse('tap test/*.test.js'),
+		['tap', { op: 'glob', pattern: 'test/*.test.js' }]
+	);
+
+	t.same(parse('tap "test/*.test.js"'), ['tap', 'test/*.test.js']);
+	t.end();
+});
Index: frontend/node_modules/shell-quote/test/parse.js
===================================================================
--- frontend/node_modules/shell-quote/test/parse.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/test/parse.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,44 @@
+'use strict';
+
+var test = require('tape');
+var parse = require('../').parse;
+
+test('parse shell commands', function (t) {
+	t.same(parse(''), [], 'parses an empty string');
+
+	t['throws'](
+		function () { parse('${}'); },
+		Error,
+		'empty substitution throws'
+	);
+	t['throws'](
+		function () { parse('${'); },
+		Error,
+		'incomplete substitution throws'
+	);
+
+	t.same(parse('a \'b\' "c"'), ['a', 'b', 'c']);
+	t.same(
+		parse('beep "boop" \'foo bar baz\' "it\'s \\"so\\" groovy"'),
+		['beep', 'boop', 'foo bar baz', 'it\'s "so" groovy']
+	);
+	t.same(parse('a b\\ c d'), ['a', 'b c', 'd']);
+	t.same(parse('\\$beep bo\\`op'), ['$beep', 'bo`op']);
+	t.same(parse('echo "foo = \\"foo\\""'), ['echo', 'foo = "foo"']);
+	t.same(parse(''), []);
+	t.same(parse(' '), []);
+	t.same(parse('\t'), []);
+	t.same(parse('a"b c d"e'), ['ab c de']);
+	t.same(parse('a\\ b"c d"\\ e f'), ['a bc d e', 'f']);
+	t.same(parse('a\\ b"c d"\\ e\'f g\' h'), ['a bc d ef g', 'h']);
+	t.same(parse("x \"bl'a\"'h'"), ['x', "bl'ah"]);
+	t.same(parse("x bl^'a^'h'", {}, { escape: '^' }), ['x', "bl'a'h"]);
+	t.same(parse('abcH def', {}, { escape: 'H' }), ['abc def']);
+
+	t.deepEqual(parse('# abc  def  ghi'), [{ comment: ' abc  def  ghi' }], 'start-of-line comment content is unparsed');
+	t.deepEqual(parse('xyz # abc  def  ghi'), ['xyz', { comment: ' abc  def  ghi' }], 'comment content is unparsed');
+
+	t.deepEqual(parse('-x "" -y'), ['-x', '', '-y'], 'empty string is preserved');
+
+	t.end();
+});
Index: frontend/node_modules/shell-quote/test/quote.js
===================================================================
--- frontend/node_modules/shell-quote/test/quote.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/test/quote.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,60 @@
+'use strict';
+
+var test = require('tape');
+var quote = require('../').quote;
+
+test('quote', function (t) {
+	t.equal(quote(['a', 'b', 'c d']), 'a b \'c d\'');
+	t.equal(
+		quote(['a', 'b', "it's a \"neat thing\""]),
+		'a b "it\'s a \\"neat thing\\""'
+	);
+	t.equal(
+		quote(['$', '`', '\'']),
+		'\\$ \\` "\'"'
+	);
+	t.equal(quote([]), '');
+	t.equal(quote(['a\nb']), "'a\nb'");
+	t.equal(quote([' #(){}*|][!']), "' #(){}*|][!'");
+	t.equal(quote(["'#(){}*|][!"]), '"\'#(){}*|][\\!"');
+	t.equal(quote(['X#(){}*|][!']), 'X\\#\\(\\)\\{\\}\\*\\|\\]\\[\\!');
+	t.equal(quote(['a\n#\nb']), "'a\n#\nb'");
+	t.equal(quote(['><;{}']), '\\>\\<\\;\\{\\}');
+	t.equal(quote(['a', 1, true, false]), 'a 1 true false');
+	t.equal(quote(['a', 1, null, undefined]), 'a 1 null undefined');
+	t.equal(quote(['a\\x']), "'a\\x'");
+	t.equal(quote(['a"b']), '\'a"b\'');
+	t.equal(quote(['"a"b"']), '\'"a"b"\'');
+	t.equal(quote(['a\\"b']), '\'a\\"b\'');
+	t.equal(quote(['a\\b']), '\'a\\b\'');
+	t.end();
+});
+
+test('quote ops', function (t) {
+	t.equal(quote(['a', { op: '|' }, 'b']), 'a \\| b');
+	t.equal(
+		quote(['a', { op: '&&' }, 'b', { op: ';' }, 'c']),
+		'a \\&\\& b \\; c'
+	);
+	t.end();
+});
+
+test('quote windows paths', { skip: 'breaking change, disabled until 2.x' }, function (t) {
+	var path = 'C:\\projects\\node-shell-quote\\index.js';
+
+	t.equal(quote([path, 'b', 'c d']), 'C:\\projects\\node-shell-quote\\index.js b \'c d\'');
+
+	t.end();
+});
+
+test("chars for windows paths don't break out", function (t) {
+	var x = '`:\\a\\b';
+	t.equal(quote([x]), "'`:\\a\\b'");
+	t.end();
+});
+
+test('empty strings', function (t) {
+	t.equal(quote(['-x', '', 'y']), '-x \'\' y');
+
+	t.end();
+});
Index: frontend/node_modules/shell-quote/test/set.js
===================================================================
--- frontend/node_modules/shell-quote/test/set.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/shell-quote/test/set.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,31 @@
+'use strict';
+
+var test = require('tape');
+var parse = require('../').parse;
+
+test('set env vars', function (t) {
+	t.same(
+		parse('ABC=444 x y z'),
+		['ABC=444', 'x', 'y', 'z']
+	);
+	t.same(
+		parse('ABC=3\\ 4\\ 5 x y z'),
+		['ABC=3 4 5', 'x', 'y', 'z']
+	);
+	t.same(
+		parse('X="7 8 9" printx'),
+		['X=7 8 9', 'printx']
+	);
+	t.same(
+		parse('X="7 8 9"; printx'),
+		['X=7 8 9', { op: ';' }, 'printx']
+	);
+	t.same(
+		parse('X="7 8 9"; printx', function () {
+			t.fail('should not have matched any keys');
+		}),
+		['X=7 8 9', { op: ';' }, 'printx']
+	);
+
+	t.end();
+});
