diff --git a/.gitignore b/.gitignore
index b2e8eafe382781f93a4217b0e0b2e1a86b844317..c4930687803e8e7afe68730a94381b9325948c73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,6 @@ node_modules
 /*.env
 !/example.env
 /.svelte-kit
-/build/
+/build
+/gitlab-ci/build
+
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a05e84544006c8f0507f219f5f3da3fbf8b6a177
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,31 @@
+default:
+  image: node:16-bullseye
+
+  cache:
+    paths:
+      - node_modules
+
+ci:
+  stage: build
+  before_script:
+    # Without apt update, apt install fails.
+    - apt update --yes
+
+    # Install ssh-agent if not already installed, it is required by Docker.
+    - "which ssh-agent || apt install -y openssh-client"
+    # Run ssh-agent (inside the build environment)
+    - eval $(ssh-agent -s)
+
+    # `esbuild` 0.13.8 fails when rebuilding it twice.
+    # => Reinstall it every time.
+    - rm -Rf node_modules/esbuild
+    #
+    - npm install
+    # Needed when Node version changes:
+    - npm rebuild
+
+    # Compile gitlab-ci TypeScript script.
+    - npx tsc --declaration --project gitlab-ci/tsconfig.json
+  script:
+    # Execute gitlab-ci JavaScript script.
+    - node --experimental-specifier-resolution=node gitlab-ci/build/gitlab-ci.js
diff --git a/gitlab-ci/src/gitlab-ci.ts b/gitlab-ci/src/gitlab-ci.ts
new file mode 100644
index 0000000000000000000000000000000000000000..999d3dde05654edfe655a6e953d14c207aa074ff
--- /dev/null
+++ b/gitlab-ci/src/gitlab-ci.ts
@@ -0,0 +1,392 @@
+import assert from "assert"
+import { $, fetch, fs } from "zx"
+
+export interface VersionObject {
+  major: number
+  minor: number
+  patch: number
+}
+
+const {
+  CI_COMMIT_BRANCH,
+  CI_COMMIT_TAG,
+  CI_COMMIT_TITLE,
+  CI_DEFAULT_BRANCH,
+  CI_JOB_TOKEN,
+  CI_PIPELINE_SOURCE,
+  CI_PROJECT_NAME,
+  CI_PROJECT_NAMESPACE,
+  CI_SERVER_HOST,
+  CI_SERVER_URL,
+  // NPM_EMAIL,
+  // NPM_TOKEN,
+  SSH_KNOWN_HOSTS,
+  SSH_PRIVATE_KEY,
+} = process.env
+const packagePath = "package.json"
+
+function checkVersionObject(
+  {
+    major: majorReference,
+    minor: minorReference,
+    patch: patchReference,
+  }: VersionObject,
+  versionObject: VersionObject | undefined,
+): boolean {
+  if (versionObject === undefined) {
+    return true
+  }
+  const { major, minor, patch } = versionObject
+  if (major < majorReference) {
+    return true
+  }
+  if (major === majorReference) {
+    if (minor < minorReference) {
+      return true
+    }
+    if (minor === minorReference) {
+      return patch <= patchReference || patch === patchReference + 1
+    }
+    return minor === minorReference + 1 && patch === 0
+  }
+  return major === majorReference + 1 && minor === 0 && patch === 0
+}
+
+async function commitAndPushWithUpdatedVersions(
+  nextVersionObject?: VersionObject | undefined,
+) {
+  if (nextVersionObject === undefined) {
+    // Retrieve next version of project.
+    nextVersionObject = await latestVersionObjectFromTags()
+    assert.notStrictEqual(nextVersionObject, undefined)
+    nextVersionObject!.patch++
+  }
+  const nextVersion = versionFromObject(nextVersionObject!)
+
+  if ((await $`git diff --quiet --staged`.exitCode) !== 0) {
+    let packageJson = await fs.readFile(packagePath, "utf-8")
+    packageJson = packageJson.replace(
+      /^  "version": "(.*?)",$/m,
+      `  "version": "${nextVersion}",`,
+    )
+    await fs.writeFile(packagePath, packageJson, "utf-8")
+  }
+
+  await $`git add .`
+  if ((await $`git diff --quiet --staged`.exitCode) !== 0) {
+    await $`git commit -m ${nextVersion}`
+    await $`git push --set-upstream`
+  }
+  await $`git tag -a ${nextVersion} -m ${nextVersion}`
+  await $`git push --set-upstream --tags`
+
+  // // Note: Don't fail when there is nothing new to publish.
+  // if (NPM_EMAIL !== undefined && NPM_TOKEN !== undefined) {
+  //   await nothrow($`npm publish`)
+  // }
+}
+
+async function configureGit() {
+  console.log("Configuring git…")
+
+  // Set the Git user name and email.
+  await $`git config --global user.email "admin+leximpact-socio-fiscal-ui-ci@tax-benefit.org"`
+  await $`git config --global user.name "Leximpact socio-fiscal UI CI"`
+
+  console.log("Git configuration completed.")
+}
+
+// async function configureNpm() {
+//   console.log("Configuring npm…")
+
+//   // Configure npm to be able to publish packages.
+//   if (NPM_EMAIL !== undefined && NPM_TOKEN !== undefined) {
+//     await $`echo ${`//registry.npmjs.org/:_authToken=${NPM_TOKEN}`} > ~/.npmrc`
+//     await $`echo ${`email=${NPM_EMAIL}`} >> ~/.npmrc`
+//   }
+
+//   console.log("Npm configuration completed.")
+// }
+
+async function configureSsh() {
+  console.log("Configuring ssh…")
+
+  // Note: `eval $(ssh-agent -s)` must be done before calling this script because it
+  // creates 2 environment variables (then used by ssh clients).
+  // // Install ssh-agent if not already installed, to be able to use git with ssh.
+  // if ((await $`which ssh-agent`.exitCode) !== 0) {
+  //   await $`apt install -y openssh-client`
+  // }
+  // // Run ssh-agent (inside the build environment)
+  // await $`eval $(ssh-agent -s)`
+
+  // Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
+  // Use `tr` to fix line endings which makes ed25519 keys work without
+  // extra base64 encoding.
+  // https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556
+  if (SSH_PRIVATE_KEY !== undefined) {
+    await $`echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -`
+  }
+
+  if (SSH_KNOWN_HOSTS !== undefined) {
+    // Create the SSH directory and give it the right permissions
+    await $`mkdir -p ~/.ssh`
+    await $`chmod 700 ~/.ssh`
+    // Accept the SSH host keys of GitLab server.
+    await $`echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts`
+    await $`chmod 644 ~/.ssh/known_hosts`
+  }
+
+  console.log("Ssh configuration completed.")
+}
+
+async function latestVersionObjectFromTags(): Promise<
+  VersionObject | undefined
+> {
+  // Retrieve all tags.
+  await $`git fetch --all --tags`
+
+  return (await $`git tag --list`).stdout
+    .split("\n")
+    .map(objectFromVersion)
+    .filter((versionObject) => versionObject !== undefined)
+    .sort((versionObject1, versionObject2) =>
+      versionObject1!.major !== versionObject2!.major
+        ? versionObject2!.major - versionObject1!.major
+        : versionObject1!.minor !== versionObject2!.minor
+        ? versionObject2!.minor - versionObject1!.minor
+        : versionObject2!.patch - versionObject1!.patch,
+    )[0]
+}
+
+async function main() {
+  console.log("Starting gitlab-ci.ts…")
+
+  await configureGit()
+  // await configureNpm()
+  await configureSsh()
+
+  console.log(`Handling "${CI_PIPELINE_SOURCE}" event…`)
+  switch (CI_PIPELINE_SOURCE) {
+    case "api":
+    case "pipeline":
+    case "schedule":
+    case "trigger":
+    case "web": {
+      if (CI_COMMIT_BRANCH === CI_DEFAULT_BRANCH) {
+        // A pipeline has been triggered (for example when OpenFisca-France has been updated or a user as
+        // launched a new pipeline manually or…).
+        // => A new version of project must be created if and only if a dependency has
+        // changed.
+
+        console.log(`Handling trigger…`)
+
+        await resetGitRepository()
+
+        // Test project with the current dependencies.
+        await $`ln -s example.env .env`
+        await $`npm run build`
+        // TODO: Add tests.
+
+        await triggerDevDeployPipeline()
+        // await triggerProdDeployPipeline()
+      } else {
+        console.log(
+          `Unhandled event "${CI_PIPELINE_SOURCE}" in branch "${CI_COMMIT_BRANCH}".`,
+        )
+      }
+      break
+    }
+    case "push": {
+      if (CI_COMMIT_BRANCH !== undefined) {
+        console.log(`Push to branch ${CI_COMMIT_BRANCH}`)
+
+        if (CI_COMMIT_BRANCH.match(/^\d+_\d+_\d+$/) != null) {
+          console.log(
+            `Ignoring commit in version branch (for branch ${CI_COMMIT_BRANCH}).`,
+          )
+        } else if (CI_COMMIT_TITLE?.match(/^\d+\.\d+\.\d+( |$)/) != null) {
+          console.log(
+            `Ignoring version commit (for version ${CI_COMMIT_TITLE}).`,
+          )
+        } else {
+          await resetGitRepository()
+
+          // Retrieve current version of project.
+          const tagVersionObject = (await latestVersionObjectFromTags()) ?? {
+            major: 0,
+            minor: 0,
+            patch: 0,
+          }
+          const tagVersion = versionFromObject(tagVersionObject)
+          let nextVersionObject = {
+            ...tagVersionObject,
+            patch: tagVersionObject!.patch + 1,
+          }
+
+          // Ensure that the version numbers of project packages are
+          // compatible with last version tag.
+          const pkg = await fs.readJson(packagePath)
+          const { version: packageVersion } = pkg
+          const packageVersionObject = objectFromVersion(packageVersion)
+          assert(
+            checkVersionObject(tagVersionObject, packageVersionObject),
+            `In ${packagePath}, project version should be compatible with ${tagVersion}, got "${packageVersion}" instead.`,
+          )
+          nextVersionObject = maxVersionObject(
+            nextVersionObject,
+            packageVersionObject,
+          )
+
+          // Test project with the current dependencies.
+          await $`ln -s example.env .env`
+          await $`npm run build`
+          // TODO: Add tests.
+
+          if (CI_COMMIT_BRANCH === CI_DEFAULT_BRANCH) {
+            // A merge request has been merged into master (ie content of project has been changed).
+            // => Create a new version of project.
+            await commitAndPushWithUpdatedVersions(nextVersionObject)
+            await triggerDevDeployPipeline()
+            // await triggerProdDeployPipeline()
+          }
+        }
+      } else if (CI_COMMIT_TAG !== undefined) {
+        console.log(`Pushing commit tag "${CI_COMMIT_TAG}"…`)
+      } else if (CI_COMMIT_TAG !== undefined) {
+        console.log(`Unhandled push event.`)
+      }
+      break
+    }
+    default:
+      console.log(
+        `Unhandled event "${CI_PIPELINE_SOURCE}" in branch "${CI_COMMIT_BRANCH}".`,
+      )
+  }
+}
+
+function maxVersionObject(
+  versionObject1: VersionObject,
+  versionObject2: VersionObject | undefined,
+): VersionObject {
+  if (versionObject2 === undefined) {
+    return versionObject1
+  }
+  const { major: major1, minor: minor1, patch: patch1 } = versionObject1
+  const { major: major2, minor: minor2, patch: patch2 } = versionObject2
+  if (major1 < major2) {
+    return versionObject2
+  }
+  if (major1 > major2) {
+    return versionObject1
+  }
+  if (minor1 < minor2) {
+    return versionObject2
+  }
+  if (minor1 > minor2) {
+    return versionObject1
+  }
+  if (patch1 < patch2) {
+    return versionObject2
+  }
+  if (patch1 > patch2) {
+    return versionObject1
+  }
+  // Both versions are the same. Return one of them.
+  return versionObject1
+}
+
+function objectFromVersion(
+  version: string | undefined,
+): VersionObject | undefined {
+  if (version === undefined) {
+    return undefined
+  }
+  const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/) as string[]
+  if (match === null) {
+    return undefined
+  }
+  return {
+    major: parseInt(match[1]),
+    minor: parseInt(match[2]),
+    patch: parseInt(match[3]),
+  }
+}
+
+async function resetGitRepository() {
+  // Reset current repo, because it may have been tranformed by a previous CI that failed.
+  await $`git switch ${CI_COMMIT_BRANCH}`
+  await $`git fetch origin ${CI_COMMIT_BRANCH}`
+  await $`git reset --hard origin/${CI_COMMIT_BRANCH}`
+  await $`git status`
+  if (CI_COMMIT_BRANCH === CI_DEFAULT_BRANCH) {
+    await $`git remote set-url origin git@${CI_SERVER_HOST}:${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}.git`
+  } else {
+    await $`git remote set-url origin https://${CI_SERVER_HOST}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}.git`
+  }
+}
+
+async function triggerDevDeployPipeline() {
+  const url = new URL(
+    `/api/v4/projects/28/trigger/pipeline`,
+    CI_SERVER_URL,
+  ).toString()
+  console.log(
+    "Triggering LexImpact socio-fiscal Dev Deploy pipeline on master branch…",
+  )
+  const response = await fetch(url, {
+    body: new URLSearchParams({
+      ref: "master",
+      token: CI_JOB_TOKEN!,
+    }).toString(),
+    headers: {
+      "Content-Type": "application/x-www-form-urlencoded",
+    },
+    method: "POST",
+  })
+  assert(
+    response.ok,
+    `Unexpected response from ${url}: ${response.status} ${
+      response.statusText
+    }\n${await response.text()}`,
+  )
+}
+
+// async function triggerProdDeployPipeline() {
+//   const url = new URL(
+//     `/api/v4/projects/29/trigger/pipeline`,
+//     CI_SERVER_URL
+//   ).toString();
+//   console.log(
+//     "Triggering LexImpact socio-fiscal Prod Deploy pipeline on master branch…"
+//   );
+//   const response = await fetch(url, {
+//     body: new URLSearchParams({
+//       ref: "master",
+//       token: CI_JOB_TOKEN!,
+//     }).toString(),
+//     headers: {
+//       "Content-Type": "application/x-www-form-urlencoded",
+//     },
+//     method: "POST",
+//   });
+//   assert(
+//     response.ok,
+//     `Unexpected response from ${url}: ${response.status} ${
+//       response.statusText
+//     }\n${await response.text()}`
+//   );
+// }
+
+function versionFromObject({ major, minor, patch }: VersionObject): string {
+  return `${major}.${minor}.${patch}`
+}
+
+main()
+  .then(() => {
+    process.exit(0)
+  })
+  .catch((error: unknown) => {
+    console.error(error)
+    process.exit(1)
+  })
diff --git a/gitlab-ci/tsconfig.json b/gitlab-ci/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..9ee50ee2752a437a3d2fef4e93dae7eec95e8d1b
--- /dev/null
+++ b/gitlab-ci/tsconfig.json
@@ -0,0 +1,64 @@
+{
+  "compilerOptions": {
+    /* Basic Options */
+    "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
+    "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
+    // "lib": [],                             /* Specify library files to be included in the compilation. */
+    // "allowJs": true,                       /* Allow javascript files to be compiled. */
+    // "checkJs": true,                       /* Report errors in .js files. */
+    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+    "declaration": true /* Generates corresponding '.d.ts' file. */,
+    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
+    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
+    // "outFile": "./",                       /* Concatenate and emit output to single file. */
+    "outDir": "build" /* Redirect output structure to the directory. */,
+    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+    // "composite": true,                     /* Enable project compilation */
+    // "incremental": true,                   /* Enable incremental compilation */
+    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
+    // "removeComments": true,                /* Do not emit comments to output. */
+    // "noEmit": true,                        /* Do not emit outputs. */
+    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
+    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+    /* Strict Type-Checking Options */
+    "strict": true /* Enable all strict type-checking options. */,
+    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
+    // "strictNullChecks": true,              /* Enable strict null checks. */
+    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
+    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
+    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
+    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
+
+    /* Additional Checks */
+    "noUnusedLocals": true /* Report errors on unused locals. */,
+    "noUnusedParameters": true /* Report errors on unused parameters. */,
+    "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
+    "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
+
+    /* Module Resolution Options */
+    "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
+    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
+    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
+    // "typeRoots": [],                       /* List of folders to include type definitions from. */    // "types": [],                           /* Type declaration files to be included in compilation. */
+    // "types": [],                           /* Type declaration files to be included in compilation. */
+    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
+    // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */
+
+    /* Source Map Options */
+    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
+    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+
+    /* Experimental Options */
+    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
+    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
+  },
+  "include": ["src/*.ts"]
+}
diff --git a/package-lock.json b/package-lock.json
index 2c132604c83feb2e7bf94d2181df2aff09bd2123..eee8e710387e762ab2deb197a366a39ff487ac04 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -54,7 +54,8 @@
         "tailwindcss": "^2.0.3",
         "tslib": "^2.0.0",
         "typescript": "^4.0.0",
-        "uuid": "^8.3.2"
+        "uuid": "^8.3.2",
+        "zx": "^4.2.0"
       }
     },
     "node_modules/@auditors/core": {
@@ -2053,6 +2054,12 @@
       "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
       "dev": true
     },
+    "node_modules/@types/minimist": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
+      "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==",
+      "dev": true
+    },
     "node_modules/@types/node": {
       "version": "16.10.5",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.5.tgz",
@@ -3348,6 +3355,12 @@
         "node": ">=10"
       }
     },
+    "node_modules/duplexer": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
+      "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
+      "dev": true
+    },
     "node_modules/ecdsa-sig-formatter": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -4004,6 +4017,21 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/event-stream": {
+      "version": "3.3.4",
+      "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
+      "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=",
+      "dev": true,
+      "dependencies": {
+        "duplexer": "~0.1.1",
+        "from": "~0",
+        "map-stream": "~0.1.0",
+        "pause-stream": "0.0.11",
+        "split": "0.3",
+        "stream-combiner": "~0.0.4",
+        "through": "~2.3.1"
+      }
+    },
     "node_modules/fast-deep-copy": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fast-deep-copy/-/fast-deep-copy-1.0.0.tgz",
@@ -4155,6 +4183,12 @@
         "url": "https://www.patreon.com/infusion"
       }
     },
+    "node_modules/from": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
+      "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=",
+      "dev": true
+    },
     "node_modules/fs-extra": {
       "version": "10.0.0",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz",
@@ -4888,6 +4922,12 @@
         "semver": "bin/semver"
       }
     },
+    "node_modules/map-stream": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
+      "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
+      "dev": true
+    },
     "node_modules/mdn-data": {
       "version": "2.0.14",
       "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
@@ -5310,6 +5350,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/pause-stream": {
+      "version": "0.0.11",
+      "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
+      "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=",
+      "dev": true,
+      "dependencies": {
+        "through": "~2.3"
+      }
+    },
     "node_modules/picocolors": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
@@ -5922,6 +5971,21 @@
         "node": ">=0.4.0"
       }
     },
+    "node_modules/ps-tree": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz",
+      "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==",
+      "dev": true,
+      "dependencies": {
+        "event-stream": "=3.3.4"
+      },
+      "bin": {
+        "ps-tree": "bin/ps-tree.js"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
     "node_modules/punycode": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -6398,6 +6462,18 @@
       "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
       "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
     },
+    "node_modules/split": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
+      "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=",
+      "dev": true,
+      "dependencies": {
+        "through": "2"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/sprintf-js": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -6410,6 +6486,15 @@
       "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
       "dev": true
     },
+    "node_modules/stream-combiner": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
+      "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=",
+      "dev": true,
+      "dependencies": {
+        "duplexer": "~0.1.1"
+      }
+    },
     "node_modules/strip-ansi": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -6835,6 +6920,12 @@
       "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
       "dev": true
     },
+    "node_modules/through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
     "node_modules/timsort": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
@@ -7174,6 +7265,145 @@
       "engines": {
         "node": ">= 6"
       }
+    },
+    "node_modules/zx": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/zx/-/zx-4.2.0.tgz",
+      "integrity": "sha512-/4f7FaJecA9I655KXKXIHO3CFNYjAz2uSmTz6v2eNlKdrQKyz4VyF3RjqFuP6nQG+Hd3+NjOvrVNBkv8Ne9d4Q==",
+      "dev": true,
+      "dependencies": {
+        "@types/fs-extra": "^9.0.12",
+        "@types/minimist": "^1.2.2",
+        "@types/node": "^16.6",
+        "@types/node-fetch": "^2.5.12",
+        "chalk": "^4.1.2",
+        "fs-extra": "^10.0.0",
+        "globby": "^12.0.1",
+        "minimist": "^1.2.5",
+        "node-fetch": "^2.6.1",
+        "ps-tree": "^1.2.0",
+        "which": "^2.0.2"
+      },
+      "bin": {
+        "zx": "zx.mjs"
+      },
+      "engines": {
+        "node": ">= 14.13.0"
+      }
+    },
+    "node_modules/zx/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/zx/node_modules/array-union": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz",
+      "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/zx/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/zx/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/zx/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true
+    },
+    "node_modules/zx/node_modules/globby": {
+      "version": "12.0.2",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-12.0.2.tgz",
+      "integrity": "sha512-lAsmb/5Lww4r7MM9nCCliDZVIKbZTavrsunAsHLr9oHthrZP1qi7/gAnHOsUs9bLvEt2vKVJhHmxuL7QbDuPdQ==",
+      "dev": true,
+      "dependencies": {
+        "array-union": "^3.0.1",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.2.7",
+        "ignore": "^5.1.8",
+        "merge2": "^1.4.1",
+        "slash": "^4.0.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/zx/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/zx/node_modules/slash": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
+      "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/zx/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
     }
   },
   "dependencies": {
@@ -8581,6 +8811,12 @@
       "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
       "dev": true
     },
+    "@types/minimist": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
+      "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==",
+      "dev": true
+    },
     "@types/node": {
       "version": "16.10.5",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.5.tgz",
@@ -9550,6 +9786,12 @@
       "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
       "dev": true
     },
+    "duplexer": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
+      "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
+      "dev": true
+    },
     "ecdsa-sig-formatter": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -9987,6 +10229,21 @@
       "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
       "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
     },
+    "event-stream": {
+      "version": "3.3.4",
+      "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz",
+      "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=",
+      "dev": true,
+      "requires": {
+        "duplexer": "~0.1.1",
+        "from": "~0",
+        "map-stream": "~0.1.0",
+        "pause-stream": "0.0.11",
+        "split": "0.3",
+        "stream-combiner": "~0.0.4",
+        "through": "~2.3.1"
+      }
+    },
     "fast-deep-copy": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fast-deep-copy/-/fast-deep-copy-1.0.0.tgz",
@@ -10110,6 +10367,12 @@
       "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==",
       "dev": true
     },
+    "from": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
+      "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=",
+      "dev": true
+    },
     "fs-extra": {
       "version": "10.0.0",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz",
@@ -10700,6 +10963,12 @@
         }
       }
     },
+    "map-stream": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
+      "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
+      "dev": true
+    },
     "mdn-data": {
       "version": "2.0.14",
       "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
@@ -11014,6 +11283,15 @@
       "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
       "dev": true
     },
+    "pause-stream": {
+      "version": "0.0.11",
+      "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
+      "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=",
+      "dev": true,
+      "requires": {
+        "through": "~2.3"
+      }
+    },
     "picocolors": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
@@ -11397,6 +11675,15 @@
       "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
       "dev": true
     },
+    "ps-tree": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz",
+      "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==",
+      "dev": true,
+      "requires": {
+        "event-stream": "=3.3.4"
+      }
+    },
     "punycode": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -11759,6 +12046,15 @@
       "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
       "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
     },
+    "split": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz",
+      "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=",
+      "dev": true,
+      "requires": {
+        "through": "2"
+      }
+    },
     "sprintf-js": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -11771,6 +12067,15 @@
       "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
       "dev": true
     },
+    "stream-combiner": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
+      "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=",
+      "dev": true,
+      "requires": {
+        "duplexer": "~0.1.1"
+      }
+    },
     "strip-ansi": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -12050,6 +12355,12 @@
       "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
       "dev": true
     },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
     "timsort": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
@@ -12295,6 +12606,102 @@
       "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
       "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
       "dev": true
+    },
+    "zx": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/zx/-/zx-4.2.0.tgz",
+      "integrity": "sha512-/4f7FaJecA9I655KXKXIHO3CFNYjAz2uSmTz6v2eNlKdrQKyz4VyF3RjqFuP6nQG+Hd3+NjOvrVNBkv8Ne9d4Q==",
+      "dev": true,
+      "requires": {
+        "@types/fs-extra": "^9.0.12",
+        "@types/minimist": "^1.2.2",
+        "@types/node": "^16.6",
+        "@types/node-fetch": "^2.5.12",
+        "chalk": "^4.1.2",
+        "fs-extra": "^10.0.0",
+        "globby": "^12.0.1",
+        "minimist": "^1.2.5",
+        "node-fetch": "^2.6.1",
+        "ps-tree": "^1.2.0",
+        "which": "^2.0.2"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "array-union": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz",
+          "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==",
+          "dev": true
+        },
+        "chalk": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true
+        },
+        "globby": {
+          "version": "12.0.2",
+          "resolved": "https://registry.npmjs.org/globby/-/globby-12.0.2.tgz",
+          "integrity": "sha512-lAsmb/5Lww4r7MM9nCCliDZVIKbZTavrsunAsHLr9oHthrZP1qi7/gAnHOsUs9bLvEt2vKVJhHmxuL7QbDuPdQ==",
+          "dev": true,
+          "requires": {
+            "array-union": "^3.0.1",
+            "dir-glob": "^3.0.1",
+            "fast-glob": "^3.2.7",
+            "ignore": "^5.1.8",
+            "merge2": "^1.4.1",
+            "slash": "^4.0.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==",
+          "dev": true
+        },
+        "slash": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
+          "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
     }
   }
 }
diff --git a/package.json b/package.json
index 57e5cc2420766fa19238c20b5653d9f35f154238..97044ebcbfc7ab8b756cea03ac5d9ec4d9183480 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,8 @@
     "tailwindcss": "^2.0.3",
     "tslib": "^2.0.0",
     "typescript": "^4.0.0",
-    "uuid": "^8.3.2"
+    "uuid": "^8.3.2",
+    "zx": "^4.2.0"
   },
   "type": "module"
 }