From 009ab8ff2633bec63b5455d9a62fedbd11e03574 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Domen=20Ko=C5=BEar?= <domen@dev.si>
Date: Mon, 10 Jul 2023 11:27:33 +0100
Subject: [PATCH] Rewrite CLI in Python

---
 .devcontainer.json              |   2 +-
 .envrc                          |   4 -
 .github/workflows/buildtest.yml |   4 +-
 .gitignore                      |  15 +-
 devenv.nix                      |  17 +-
 flake.lock                      |  91 ++++++-
 flake.nix                       |   8 +-
 package.nix                     |  29 +++
 poetry.lock                     | 392 +++++++++++++---------------
 pyproject.toml                  |  24 +-
 src/devenv-yaml.nix             |  47 ----
 src/devenv.nix                  | 302 ----------------------
 src/devenv/__init__.py          |   4 +
 src/devenv/cli.py               | 445 ++++++++++++++++++++++++++++++++
 src/devenv/yaml.py              |  44 ++++
 src/flake.nix                   |  85 ------
 src/modules/flake.tmpl.nix      |  91 +++++++
 17 files changed, 916 insertions(+), 688 deletions(-)
 create mode 100644 package.nix
 delete mode 100644 src/devenv-yaml.nix
 delete mode 100644 src/devenv.nix
 create mode 100644 src/devenv/__init__.py
 create mode 100644 src/devenv/cli.py
 create mode 100644 src/devenv/yaml.py
 delete mode 100644 src/flake.nix
 create mode 100644 src/modules/flake.tmpl.nix

diff --git a/.devcontainer.json b/.devcontainer.json
index e76098216..e3c39242c 100644
--- a/.devcontainer.json
+++ b/.devcontainer.json
@@ -2,7 +2,7 @@
   "customizations": {
     "vscode": {
       "extensions": [
-        "bbenoist.Nix"
+        "jnoortheen.nix-ide"
       ]
     }
   },
diff --git a/.envrc b/.envrc
index d0de725ed..e207b3c3b 100755
--- a/.envrc
+++ b/.envrc
@@ -3,10 +3,6 @@
 # Used by https://direnv.net
 set -euo pipefail
 
-# Use our own last built devenv/nix in CLI
-devenv_bin=$(nix build --print-out-paths --accept-flake-config)
-PATH_add "$devenv_bin/bin"
-
 # External users should use `source_url` to load this file
 source_env ./direnvrc
 
diff --git a/.github/workflows/buildtest.yml b/.github/workflows/buildtest.yml
index 39c165b66..93bba6813 100644
--- a/.github/workflows/buildtest.yml
+++ b/.github/workflows/buildtest.yml
@@ -75,6 +75,7 @@ jobs:
     - run: devenv shell devenv-test-example ${{ matrix.example }}
   direnv:
     name: direnv (${{ join(matrix.os) }})
+    needs: build
     strategy:
       fail-fast: false
       matrix:
@@ -99,7 +100,7 @@ jobs:
           }
         ' ./examples/simple/devenv.yaml.orig > ./examples/simple/devenv.yaml
         nix profile remove '.*'
-        nix profile install . 'nixpkgs#direnv'
+        nix profile install . 'nixpkgs#direnv' nix profile install --accept-flake-config
         mkdir -p ~/.config/direnv/
         cat > ~/.config/direnv/direnv.toml << 'EOF'
         [global]
@@ -109,6 +110,7 @@ jobs:
         direnv exec ./examples/simple true
   fish-zsh:
     name: zsh/fish (${{ join(matrix.os) }})
+    needs: build
     strategy:
       fail-fast: false
       matrix:
diff --git a/.gitignore b/.gitignore
index 52d186c15..e79e83657 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,13 @@
+# Nix & devenv
 result
-.env*
+/.env*
 .devenv*
 /.cache
 /.pre-commit-config.yaml
-/bin
-/include
-/lib
-pyvenv.cfg
-/.direnv
-/.venv
+
+# examples
 examples/rust/app/target
+
+# Python
+/.venv
+*.pyc
diff --git a/devenv.nix b/devenv.nix
index 005ee3afd..e8fb126b9 100644
--- a/devenv.nix
+++ b/devenv.nix
@@ -1,8 +1,11 @@
 { inputs, pkgs, lib, config, ... }:
 
 {
+  env = {
+    DEVENV_NIX = inputs.nix.packages.${pkgs.stdenv.system}.nix;
+  };
+
   packages = [
-    (import ./src/devenv.nix { inherit pkgs; nix = inputs.nix; })
     pkgs.cairo
     pkgs.xorg.libxcb
     pkgs.yaml2json
@@ -10,8 +13,9 @@
 
   languages.nix.enable = true;
   languages.python.enable = true;
-  languages.python.venv.enable = true;
   languages.python.poetry.enable = true;
+  languages.python.poetry.install.installRootPackage = true;
+  languages.python.poetry.install.groups = [ "docs" ];
 
   devcontainer.enable = true;
   devcontainer.settings.customizations.vscode.extensions = [ "jnoortheen.nix-ide" ];
@@ -21,7 +25,6 @@
 
   # bin/mkdocs serve --config-file mkdocs.insiders.yml
   processes.docs.exec = "mkdocs serve";
-  processes.build.exec = "${pkgs.watchexec}/bin/watchexec -e nix nix build";
 
   scripts.devenv-bump-version.exec = ''
     # TODO: ask for the new version
@@ -51,7 +54,7 @@
       nix flake init --template ''${DEVENV_ROOT}#simple
       nix flake update \
         --override-input devenv ''${DEVENV_ROOT}
-      nix develop --impure --command echo nix-develop started succesfully |& tee ./console
+      nix develop --accept-flake-config --impure --command echo nix-develop started succesfully |& tee ./console
       grep -F 'nix-develop started succesfully' <./console
       grep -F "$(${lib.getExe pkgs.hello})" <./console
 
@@ -71,15 +74,13 @@
       nix flake init --template ''${DEVENV_ROOT}#flake-parts
       nix flake update \
         --override-input devenv ''${DEVENV_ROOT}
-      nix develop --impure --command echo nix-develop started succesfully |& tee ./console
+      nix develop --accept-flake-config --impure --command echo nix-develop started succesfully |& tee ./console
       grep -F 'nix-develop started succesfully' <./console
       grep -F "$(${lib.getExe pkgs.hello})" <./console
       # Test that a container can be built
-      nix build --impure .#container-processes
+      nix build --impure --accept-flake-config .#container-processes
     popd
     rm -rf "$tmp"
-
-    # TODO: test DIRENV_ACTIVE
   '';
   scripts.devenv-test-all-examples.exec = ''
     for dir in $(ls examples); do
diff --git a/flake.lock b/flake.lock
index 0e8da4153..870bf5b5c 100644
--- a/flake.lock
+++ b/flake.lock
@@ -20,6 +20,24 @@
       "inputs": {
         "systems": "systems"
       },
+      "locked": {
+        "lastModified": 1689068808,
+        "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "flake-utils_2": {
+      "inputs": {
+        "systems": "systems_2"
+      },
       "locked": {
         "lastModified": 1685518550,
         "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
@@ -94,13 +112,34 @@
         "type": "github"
       }
     },
+    "nix-github-actions": {
+      "inputs": {
+        "nixpkgs": [
+          "poetry2nix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1688870561,
+        "narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
+        "owner": "nix-community",
+        "repo": "nix-github-actions",
+        "rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-community",
+        "repo": "nix-github-actions",
+        "type": "github"
+      }
+    },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1678875422,
-        "narHash": "sha256-T3o6NcQPwXjxJMn2shz86Chch4ljXgZn746c2caGxd8=",
+        "lastModified": 1689844446,
+        "narHash": "sha256-ud/6XYWbXFAJuTTApWyYlFtlc54NAxChS1T9Ns+qT7M=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "126f49a01de5b7e35a43fd43f891ecf6d3a51459",
+        "rev": "2d82894fa1e2d23a22f40275a78bfbb09b92ffde",
         "type": "github"
       },
       "original": {
@@ -142,12 +181,34 @@
         "type": "github"
       }
     },
+    "poetry2nix": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "nix-github-actions": "nix-github-actions",
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1689849924,
+        "narHash": "sha256-d259Z2S7CS7Na04qQNQ6LYQILuI7cf4Rpe76qc4mz40=",
+        "owner": "nix-community",
+        "repo": "poetry2nix",
+        "rev": "1d7eda9336f336392d24e9602be5cb9be7ae405c",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-community",
+        "repo": "poetry2nix",
+        "type": "github"
+      }
+    },
     "pre-commit-hooks": {
       "inputs": {
         "flake-compat": [
           "flake-compat"
         ],
-        "flake-utils": "flake-utils",
+        "flake-utils": "flake-utils_2",
         "gitignore": "gitignore",
         "nixpkgs": [
           "nixpkgs"
@@ -155,11 +216,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1688056373,
-        "narHash": "sha256-2+SDlNRTKsgo3LBRiMUcoEUb6sDViRNQhzJquZ4koOI=",
+        "lastModified": 1689668210,
+        "narHash": "sha256-XAATwDkaUxH958yXLs1lcEOmU6pSEIkatY3qjqk8X0E=",
         "owner": "cachix",
         "repo": "pre-commit-hooks.nix",
-        "rev": "5843cf069272d92b60c3ed9e55b7a8989c01d4c7",
+        "rev": "eb433bff05b285258be76513add6f6c57b441775",
         "type": "github"
       },
       "original": {
@@ -173,6 +234,7 @@
         "flake-compat": "flake-compat",
         "nix": "nix",
         "nixpkgs": "nixpkgs",
+        "poetry2nix": "poetry2nix",
         "pre-commit-hooks": "pre-commit-hooks"
       }
     },
@@ -190,6 +252,21 @@
         "repo": "default",
         "type": "github"
       }
+    },
+    "systems_2": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
     }
   },
   "root": "root",
diff --git a/flake.nix b/flake.nix
index e147e8299..338bc0270 100644
--- a/flake.nix
+++ b/flake.nix
@@ -22,14 +22,16 @@
     url = "github:domenkozar/nix/relaxed-flakes";
     inputs.nixpkgs.follows = "nixpkgs";
   };
+  inputs.poetry2nix = {
+    url = "github:nix-community/poetry2nix";
+    inputs.nixpkgs.follows = "nixpkgs";
+  };
 
   outputs = { self, nixpkgs, pre-commit-hooks, nix, ... }@inputs:
     let
       systems = [ "x86_64-linux" "i686-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
       forAllSystems = f: builtins.listToAttrs (map (name: { inherit name; value = f name; }) systems);
-      mkPackage = pkgs: import ./src/devenv.nix {
-        inherit pkgs nix;
-      };
+      mkPackage = pkgs: import ./package.nix { inherit pkgs inputs; };
       mkDevShellPackage = config: pkgs: import ./src/devenv-devShell.nix { inherit config pkgs; };
       mkDocOptions = pkgs:
         let
diff --git a/package.nix b/package.nix
new file mode 100644
index 000000000..09b5f9a9f
--- /dev/null
+++ b/package.nix
@@ -0,0 +1,29 @@
+{ pkgs, inputs }:
+
+let
+  python = (pkgs.python3.override {
+    readline = null;
+    ncurses = null;
+    gdbm = null;
+    sqlite = null;
+    tzdata = null;
+    stripConfig = true;
+    stripIdlelib = true;
+    stripTests = true;
+    stripTkinter = true;
+    rebuildBytecode = false;
+    stripBytecode = true;
+    includeSiteCustomize = false;
+    enableOptimizations = false;
+    enableLTO = false;
+    mimetypesSupport = false;
+  }).overrideAttrs (_: { pname = "python3-minimal"; });
+in
+(inputs.poetry2nix.legacyPackages.${pkgs.stdenv.system}.mkPoetryApplication {
+  projectDir = ./.;
+  inherit python;
+}).overrideAttrs (old: {
+  makeWrapperArgs = [
+    "--set DEVENV_NIX ${inputs.nix.packages.${pkgs.stdenv.system}.nix}"
+  ];
+})
diff --git a/poetry.lock b/poetry.lock
index d9029e8bb..289f8ce6c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,10 +1,9 @@
-# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
 
 [[package]]
 name = "cairocffi"
 version = "1.6.0"
 description = "cffi-based cairo bindings for Python"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -24,7 +23,6 @@ xcb = ["xcffib (>=1.4.0)"]
 name = "cairosvg"
 version = "2.7.0"
 description = "A Simple SVG Converter based on Cairo"
-category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -47,7 +45,6 @@ test = ["flake8", "isort", "pytest"]
 name = "certifi"
 version = "2023.5.7"
 description = "Python package for providing Mozilla's CA Bundle."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -59,7 +56,6 @@ files = [
 name = "cffi"
 version = "1.15.1"
 description = "Foreign Function Interface for Python calling C code."
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -134,110 +130,106 @@ pycparser = "*"
 
 [[package]]
 name = "charset-normalizer"
-version = "3.1.0"
+version = "3.2.0"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "main"
 optional = false
 python-versions = ">=3.7.0"
 files = [
-    {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"},
-    {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"},
-    {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"},
-    {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"},
-    {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"},
-    {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"},
-    {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"},
+    {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"},
+    {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"},
+    {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"},
+    {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"},
+    {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"},
+    {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"},
+    {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"},
 ]
 
 [[package]]
 name = "click"
-version = "8.1.3"
+version = "8.1.6"
 description = "Composable command line interface toolkit"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
-    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
+    {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"},
+    {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"},
 ]
 
 [package.dependencies]
 colorama = {version = "*", markers = "platform_system == \"Windows\""}
-importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
 
 [[package]]
 name = "colorama"
 version = "0.4.6"
 description = "Cross-platform colored terminal text."
-category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 files = [
@@ -249,7 +241,6 @@ files = [
 name = "cssselect2"
 version = "0.7.0"
 description = "CSS selectors for Python ElementTree"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -269,7 +260,6 @@ test = ["flake8", "isort", "pytest"]
 name = "defusedxml"
 version = "0.7.1"
 description = "XML bomb protection for Python stdlib modules"
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 files = [
@@ -281,7 +271,6 @@ files = [
 name = "ghp-import"
 version = "2.1.0"
 description = "Copy your docs directly to the gh-pages branch."
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -299,7 +288,6 @@ dev = ["flake8", "markdown", "twine", "wheel"]
 name = "gitdb"
 version = "4.0.10"
 description = "Git Object Database"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -312,25 +300,22 @@ smmap = ">=3.0.1,<6"
 
 [[package]]
 name = "gitpython"
-version = "3.1.31"
+version = "3.1.32"
 description = "GitPython is a Python library used to interact with Git repositories"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "GitPython-3.1.31-py3-none-any.whl", hash = "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d"},
-    {file = "GitPython-3.1.31.tar.gz", hash = "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573"},
+    {file = "GitPython-3.1.32-py3-none-any.whl", hash = "sha256:e3d59b1c2c6ebb9dfa7a184daf3b6dd4914237e7488a1730a6d8f6f5d0b4187f"},
+    {file = "GitPython-3.1.32.tar.gz", hash = "sha256:8d9b8cb1e80b9735e8717c9362079d3ce4c6e5ddeebedd0361b228c3a67a62f6"},
 ]
 
 [package.dependencies]
 gitdb = ">=4.0.1,<5"
-typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""}
 
 [[package]]
 name = "idna"
 version = "3.4"
 description = "Internationalized Domain Names in Applications (IDNA)"
-category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -340,30 +325,27 @@ files = [
 
 [[package]]
 name = "importlib-metadata"
-version = "6.7.0"
+version = "6.8.0"
 description = "Read metadata from Python packages"
-category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"},
-    {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"},
+    {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"},
+    {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"},
 ]
 
 [package.dependencies]
-typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
 zipp = ">=0.5"
 
 [package.extras]
 docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
 perf = ["ipython"]
-testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
+testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
 
 [[package]]
 name = "jinja2"
 version = "3.1.2"
 description = "A very fast and expressive template engine."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -381,7 +363,6 @@ i18n = ["Babel (>=2.7)"]
 name = "markdown"
 version = "3.3.7"
 description = "Python implementation of Markdown."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -399,7 +380,6 @@ testing = ["coverage", "pyyaml"]
 name = "markupsafe"
 version = "2.1.3"
 description = "Safely add untrusted strings to HTML/XML markup."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -459,7 +439,6 @@ files = [
 name = "mergedeep"
 version = "1.3.4"
 description = "A deep merge function for 🐍."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -471,7 +450,6 @@ files = [
 name = "mkdocs"
 version = "1.4.3"
 description = "Project documentation with Markdown."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -490,7 +468,6 @@ mergedeep = ">=1.3.4"
 packaging = ">=20.5"
 pyyaml = ">=5.1"
 pyyaml-env-tag = ">=0.1"
-typing-extensions = {version = ">=3.10", markers = "python_version < \"3.8\""}
 watchdog = ">=2.0"
 
 [package.extras]
@@ -501,7 +478,6 @@ min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-imp
 name = "mkdocs-include-markdown-plugin"
 version = "4.0.4"
 description = "Mkdocs Markdown includer plugin."
-category = "main"
 optional = false
 python-versions = "<3.12,>=3.7"
 files = [
@@ -513,7 +489,6 @@ files = [
 name = "mkdocs-markdownextradata-plugin"
 version = "0.2.5"
 description = "A MkDocs plugin that injects the mkdocs.yml extra variables into the markdown template"
-category = "main"
 optional = false
 python-versions = ">=2.7.9,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
 files = [
@@ -526,14 +501,13 @@ pyyaml = "*"
 
 [[package]]
 name = "mkdocs-material"
-version = "9.1.18"
+version = "9.1.19"
 description = "Documentation that simply works"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "mkdocs_material-9.1.18-py3-none-any.whl", hash = "sha256:5bcf8fb79ac2f253c0ffe93fa181cba87718c6438f459dc4180ac7418cc9a450"},
-    {file = "mkdocs_material-9.1.18.tar.gz", hash = "sha256:981dd39979723d4cda7cfc77bbbe5e54922d5761a7af23fb8ba9edb52f114b13"},
+    {file = "mkdocs_material-9.1.19-py3-none-any.whl", hash = "sha256:fb0a149294b319aedf36983919d8c40c9e566db21ead16258e20ebd2e6c0961c"},
+    {file = "mkdocs_material-9.1.19.tar.gz", hash = "sha256:73b94b08c765e92a80645aac58d6a741fc5f587deec2b715489c714827b15a6f"},
 ]
 
 [package.dependencies]
@@ -551,7 +525,6 @@ requests = ">=2.26"
 name = "mkdocs-material-extensions"
 version = "1.1.1"
 description = "Extension pack for Python Markdown and MkDocs Material."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -563,7 +536,6 @@ files = [
 name = "mkdocs-rss-plugin"
 version = "1.5.0"
 description = "MkDocs plugin which generates a static RSS feed using git log and page.meta."
-category = "main"
 optional = false
 python-versions = ">=3.7, <4"
 files = []
@@ -572,12 +544,11 @@ develop = false
 [package.dependencies]
 GitPython = ">=3.1,<3.2"
 mkdocs = ">=1.1,<2"
-pytz = {version = ">=2022.0.0,<2023.0.0", markers = "python_version < \"3.9\""}
-tzdata = {version = ">=2022.0.0,<2023.0.0", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""}
+tzdata = {version = "==2022.*", markers = "python_version >= \"3.9\" and sys_platform == \"win32\""}
 
 [package.extras]
-dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (>=4.0.0,<4.1.0)", "validator-collection (>=1.5,<1.6)"]
-doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (>=0.5.0,<0.6.0)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"]
+dev = ["black", "feedparser (>=6.0,<6.1)", "flake8 (>=4,<5.1)", "pre-commit (>=2.10,<2.21)", "pytest-cov (==4.0.*)", "validator-collection (>=1.5,<1.6)"]
+doc = ["mkdocs-bootswatch (>=1,<2)", "mkdocs-minify-plugin (==0.5.*)", "pygments (>=2.5,<3)", "pymdown-extensions (>=7,<10)"]
 
 [package.source]
 type = "git"
@@ -589,7 +560,6 @@ resolved_reference = "bac5e8935196011b4fb16c5076113d5e3ff13806"
 name = "packaging"
 version = "23.1"
 description = "Core utilities for Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -601,7 +571,6 @@ files = [
 name = "pillow"
 version = "9.5.0"
 description = "Python Imaging Library (Fork)"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -681,7 +650,6 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
 name = "pycparser"
 version = "2.21"
 description = "C parser in Python"
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -693,7 +661,6 @@ files = [
 name = "pygments"
 version = "2.15.1"
 description = "Pygments is a syntax highlighting package written in Python."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -706,14 +673,13 @@ plugins = ["importlib-metadata"]
 
 [[package]]
 name = "pymdown-extensions"
-version = "10.0.1"
+version = "10.1"
 description = "Extension pack for Python Markdown."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pymdown_extensions-10.0.1-py3-none-any.whl", hash = "sha256:ae66d84013c5d027ce055693e09a4628b67e9dec5bce05727e45b0918e36f274"},
-    {file = "pymdown_extensions-10.0.1.tar.gz", hash = "sha256:b44e1093a43b8a975eae17b03c3a77aad4681b3b56fce60ce746dbef1944c8cb"},
+    {file = "pymdown_extensions-10.1-py3-none-any.whl", hash = "sha256:ef25dbbae530e8f67575d222b75ff0649b1e841e22c2ae9a20bad9472c2207dc"},
+    {file = "pymdown_extensions-10.1.tar.gz", hash = "sha256:508009b211373058debb8247e168de4cbcb91b1bff7b5e961b2c3e864e00b195"},
 ]
 
 [package.dependencies]
@@ -724,7 +690,6 @@ pyyaml = "*"
 name = "python-dateutil"
 version = "2.8.2"
 description = "Extensions to the standard Python datetime module"
-category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 files = [
@@ -735,73 +700,59 @@ files = [
 [package.dependencies]
 six = ">=1.5"
 
-[[package]]
-name = "pytz"
-version = "2022.7.1"
-description = "World timezone definitions, modern and historical"
-category = "main"
-optional = false
-python-versions = "*"
-files = [
-    {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"},
-    {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"},
-]
-
 [[package]]
 name = "pyyaml"
-version = "6.0"
+version = "6.0.1"
 description = "YAML parser and emitter for Python"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
-    {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
-    {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
-    {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
-    {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"},
-    {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"},
-    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"},
-    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"},
-    {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"},
-    {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"},
-    {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"},
-    {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
-    {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
-    {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
-    {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
-    {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
-    {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
-    {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
-    {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
-    {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
-    {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
-    {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
-    {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
-    {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
-    {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+    {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+    {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
 ]
 
 [[package]]
 name = "pyyaml-env-tag"
 version = "0.1"
 description = "A custom YAML tag for referencing environment variables in YAML files. "
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -816,7 +767,6 @@ pyyaml = "*"
 name = "regex"
 version = "2023.6.3"
 description = "Alternative regular expression module, to replace re."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -914,7 +864,6 @@ files = [
 name = "requests"
 version = "2.31.0"
 description = "Python HTTP for Humans."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -936,7 +885,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 name = "six"
 version = "1.16.0"
 description = "Python 2 and 3 compatibility utilities"
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
@@ -948,7 +896,6 @@ files = [
 name = "smmap"
 version = "5.0.0"
 description = "A pure Python implementation of a sliding window memory map manager"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -956,11 +903,35 @@ files = [
     {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
 ]
 
+[[package]]
+name = "strictyaml"
+version = "1.7.3"
+description = "Strict, typed YAML parser"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+    {file = "strictyaml-1.7.3-py3-none-any.whl", hash = "sha256:fb5c8a4edb43bebb765959e420f9b3978d7f1af88c80606c03fb420888f5d1c7"},
+    {file = "strictyaml-1.7.3.tar.gz", hash = "sha256:22f854a5fcab42b5ddba8030a0e4be51ca89af0267961c8d6cfa86395586c407"},
+]
+
+[package.dependencies]
+python-dateutil = ">=2.6.0"
+
+[[package]]
+name = "terminaltables"
+version = "3.1.10"
+description = "Generate simple tables in terminals from a nested list of strings."
+optional = false
+python-versions = ">=2.6"
+files = [
+    {file = "terminaltables-3.1.10-py2.py3-none-any.whl", hash = "sha256:e4fdc4179c9e4aab5f674d80f09d76fa436b96fdc698a8505e0a36bf0804a874"},
+    {file = "terminaltables-3.1.10.tar.gz", hash = "sha256:ba6eca5cb5ba02bba4c9f4f985af80c54ec3dccf94cfcd190154386255e47543"},
+]
+
 [[package]]
 name = "tinycss2"
 version = "1.2.1"
 description = "A tiny CSS parser"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -975,23 +946,10 @@ webencodings = ">=0.4"
 doc = ["sphinx", "sphinx_rtd_theme"]
 test = ["flake8", "isort", "pytest"]
 
-[[package]]
-name = "typing-extensions"
-version = "4.7.1"
-description = "Backported and Experimental Type Hints for Python 3.7+"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
-    {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
-]
-
 [[package]]
 name = "tzdata"
 version = "2022.7"
 description = "Provider of IANA time zone data"
-category = "main"
 optional = false
 python-versions = ">=2"
 files = [
@@ -1001,14 +959,13 @@ files = [
 
 [[package]]
 name = "urllib3"
-version = "2.0.3"
+version = "2.0.4"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "urllib3-2.0.3-py3-none-any.whl", hash = "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1"},
-    {file = "urllib3-2.0.3.tar.gz", hash = "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"},
+    {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"},
+    {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"},
 ]
 
 [package.extras]
@@ -1021,7 +978,6 @@ zstd = ["zstandard (>=0.18.0)"]
 name = "watchdog"
 version = "3.0.0"
 description = "Filesystem events monitoring"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1061,7 +1017,6 @@ watchmedo = ["PyYAML (>=3.10)"]
 name = "webencodings"
 version = "0.5.1"
 description = "Character encoding aliases for legacy web content"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -1071,21 +1026,20 @@ files = [
 
 [[package]]
 name = "zipp"
-version = "3.15.0"
+version = "3.16.2"
 description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
-    {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
+    {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"},
+    {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"},
 ]
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
 
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.9,<3.12"
-content-hash = "9747b6c0d68ebabcf88721e81fa39c89b5529282e1d62761c09003663b966ee1"
+content-hash = "fafe77fcec2b5b4522202effc0e55be4e05c9033c9049e20a98e674735e82fcf"
diff --git a/pyproject.toml b/pyproject.toml
index 2d4083491..9aa42f39d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,12 +1,26 @@
 [tool.poetry]
 name = "devenv"
-version = "0"
-description = ""
-authors = ["Domen Kožar <domen@dev.si>"]
+version = "1.0"
+description = "Fast, Declarative, Reproducible, and Composable Developer Environments using Nix"
+authors = ["Domen Kožar <domen@cachix.org>"]
 readme = "README.md"
+repository = "https://github.com/cachix/devenv"
+packages = [{include = "devenv", from = "src"}]
+include = [
+    { path = "src", format = ["sdist", "wheel"] },
+    { path = "examples/simple", format = ["sdist", "wheel"] }
+]
 
 [tool.poetry.dependencies]
-python = ">=3.7,<3.12"
+python = ">=3.9,<3.12"
+click = "^8.1.4"
+strictyaml = "^1.7.3"
+terminaltables = "^3.1.10"
+
+[tool.poetry.group.docs]
+optional = true
+
+[tool.poetry.group.docs.dependencies]
 pillow = "^9.5.0"
 cairosvg = "^2.7.0"
 mkdocs-markdownextradata-plugin = "^0.2.5"
@@ -14,6 +28,8 @@ mkdocs-include-markdown-plugin = "^4.0.4"
 mkdocs-rss-plugin = {git = "https://github.com/Guts/mkdocs-rss-plugin", rev = "feature/no-git-no-cry"}
 mkdocs-material = "^9.1.19"
 
+[tool.poetry.scripts]
+devenv = "devenv.cli:cli"
 
 [build-system]
 requires = ["poetry-core"]
diff --git a/src/devenv-yaml.nix b/src/devenv-yaml.nix
deleted file mode 100644
index 2f920495e..000000000
--- a/src/devenv-yaml.nix
+++ /dev/null
@@ -1,47 +0,0 @@
-{ pkgs }:
-
-pkgs.writers.writePython3Bin "devenv-yaml" { libraries = with pkgs.python3Packages; [ strictyaml path ]; } ''
-  from strictyaml import Map, MapPattern, Str, Seq
-  from strictyaml import load, Bool, Any, Optional, YAMLError
-  import json
-  import sys
-  import os
-  from path import Path
-
-  inputsSchema = MapPattern(Str(), Map({
-      "url": Str(),
-      Optional("flake", default=None): Bool(),
-      Optional("inputs", default=None): Any(),
-      Optional("overlays", default=None): Seq(Str())
-  }))
-
-  schema = Map({
-      Optional("inputs", default=None): inputsSchema,
-      Optional("allowUnfree", default=False): Bool(),
-      Optional("imports", default=None): Seq(Str()),
-      Optional("permittedInsecurePackages", default=None): Seq(Str())
-  })
-
-  filename = Path("devenv.yaml").bytes().decode('utf8')
-  try:
-      devenv = load(filename, schema, label="devenv.yaml").data
-  except YAMLError as error:
-      print("Error in `devenv.yaml`", error)
-      sys.exit(1)
-
-  inputs = {}
-  for input, attrs in devenv.get('inputs', {}).items():
-      inputs[input] = {k: attrs[k] for k in ('url', 'inputs', 'flake')
-                       if k in attrs}
-
-  devenv_dir = sys.argv[1]
-
-  with open(os.path.join(devenv_dir, "flake.json"), 'w') as f:
-      f.write(json.dumps(inputs))
-
-  with open(os.path.join(devenv_dir, "devenv.json"), 'w') as f:
-      f.write(json.dumps(devenv))
-
-  with open(os.path.join(devenv_dir, "imports.txt"), 'w') as f:
-      f.write("\n".join(devenv.get('imports', [])))
-''
diff --git a/src/devenv.nix b/src/devenv.nix
deleted file mode 100644
index 2e307b836..000000000
--- a/src/devenv.nix
+++ /dev/null
@@ -1,302 +0,0 @@
-{ pkgs, ... } @ args:
-let
-  examples = ../examples;
-  lib = pkgs.lib;
-  version = lib.fileContents ./modules/latest-version;
-  inherit (pkgs)
-    bash
-    coreutils
-    findutils
-    gnugrep
-    jq
-    docopts
-    util-linuxMinimal;
-  devenv-yaml = import ./devenv-yaml.nix { inherit pkgs; };
-  nix = args.nix.packages.${pkgs.stdenv.system}.nix;
-in
-pkgs.writeScriptBin "devenv" ''
-  #!${bash}/bin/bash
-
-  # we want subshells to fail the program
-  set -e
-
-  NIX_FLAGS="--show-trace --extra-experimental-features nix-command --extra-experimental-features flakes --option warn-dirty false"
-
-  # current hack to test if we have resolved all Nix annoyances
-  export FLAKE_FILE=.devenv.flake.nix
-  export FLAKE_LOCK=devenv.lock
-
-  function assemble {
-    if [[ ! -f devenv.nix ]]; then
-      echo "File devenv.nix does not exist. To get started, run:"
-      echo
-      echo "  $ devenv init"
-      exit 1
-    fi
-
-    export DEVENV_DIR="$(pwd)/.devenv"
-    export DEVENV_GC="$DEVENV_DIR/gc"
-    ${coreutils}/bin/mkdir -p "$DEVENV_GC"
-    if [[ -f devenv.yaml ]]; then
-      ${devenv-yaml}/bin/devenv-yaml "$DEVENV_DIR"
-    else
-      [[ -f "$DEVENV_DIR/devenv.json" ]] && ${coreutils}/bin/rm "$DEVENV_DIR/devenv.json"
-      [[ -f "$DEVENV_DIR/flake.json" ]] && ${coreutils}/bin/rm "$DEVENV_DIR/flake.json"
-      [[ -f "$DEVENV_DIR/imports.txt" ]] && ${coreutils}/bin/rm "$DEVENV_DIR/imports.txt"
-    fi
-    ${coreutils}/bin/cp -f ${import ./flake.nix { inherit pkgs version; }} "$FLAKE_FILE"
-    ${coreutils}/bin/chmod +w "$FLAKE_FILE"
-  }
-
-  if [[ -z "$XDG_DATA_HOME" ]]; then
-    GC_ROOT="$HOME/.devenv/gc"
-  else 
-    GC_ROOT="$XDG_DATA_HOME/devenv/gc"
-  fi
-
-  ${coreutils}/bin/mkdir -p "$GC_ROOT"
-  GC_DIR="$GC_ROOT/$(${coreutils}/bin/date +%s%3N)"
-
-  function add_gc {
-    name=$1
-    storePath=$2
-
-    ${nix}/bin/nix-store --add-root "$DEVENV_GC/$name" -r $storePath >/dev/null
-    ${coreutils}/bin/ln -sf $storePath "$GC_DIR-$name"
-  }
-
-  function shell {
-    assemble
-    echo "Building shell ..." 1>&2
-    env=$(${nix}/bin/nix $NIX_FLAGS print-dev-env --impure --profile "$DEVENV_GC/shell")
-    ${nix}/bin/nix-env -p "$DEVENV_GC/shell" --delete-generations old 2>/dev/null
-    ${coreutils}/bin/ln -sf $(${coreutils}/bin/readlink -f "$DEVENV_GC/shell") "$GC_DIR-shell"
-  }
-
-  command=$1
-  if [[ ! -z $command ]]; then
-    shift
-  fi
-
-  case $command in
-    up)
-      shell
-      eval "$env"
-      procfilescript=$(${nix}/bin/nix $NIX_FLAGS build --no-link --print-out-paths --impure '.#procfileScript')
-      if [ "$(${coreutils}/bin/cat $procfilescript|tail -n +2)" = "" ]; then
-        echo "No 'processes' option defined: https://devenv.sh/processes/"  
-        exit 1
-      else
-        add_gc procfilescript $procfilescript
-        $procfilescript
-      fi
-      ;;
-    assemble)
-      assemble
-      ;;
-    print-dev-env)
-      shell
-      echo "$env"
-      ;;
-    shell)
-      shell
-      if [ $# -eq 0 ]; then
-        echo "Entering shell ..." 1>&2
-        echo "" 1>&2
-        ${nix}/bin/nix $NIX_FLAGS develop "$DEVENV_GC/shell"
-      else
-        set -e
-        ${nix}/bin/nix $NIX_FLAGS develop "$DEVENV_GC/shell" -c "$@"
-      fi
-      ;;
-    container)
-      assemble
-      help=$(${coreutils}/bin/cat << 'EOF'
-  Usage: container [options] CONTAINER-NAME
-
-  Options:
-    --registry=<reg>   Registry to copy the container to.
-    --copy             Copy the container to the registry.
-    --copy-args=<args> Arguments passed to `skopeo copy`.
-    --docker-run       Execute `docker run`.
-  EOF
-      )
-
-      eval "$(${docopts}/bin/docopts -A subcommand -h "$help" : "$@")"
-
-      export DEVENV_CONTAINER=1
-      container="''${subcommand[CONTAINER-NAME]}"
-
-      # build container
-      spec=$(${nix}/bin/nix $NIX_FLAGS build --impure --print-out-paths --no-link ".#devenv.containers.\"$container\".derivation")
-      echo $spec
-    
-      # copy container
-      if [[ ''${subcommand[--copy]} != false || ''${subcommand[--docker-run]} != false ]]; then
-        copyScript=$(${nix}/bin/nix $NIX_FLAGS build --print-out-paths --no-link --impure ".#devenv.containers.\"$container\".copyScript")
-
-        if [[ ''${subcommand[--docker-run]} == true ]]; then
-          registry=docker-daemon:
-        else
-          registry="''${subcommand[--registry]}"
-        fi
-        $copyScript $spec $registry ''${subcommand[--copy-args]}
-      fi
-
-      # docker run
-      if [[ ''${subcommand[--docker-run]} != false ]]; then
-        $(${nix}/bin/nix $NIX_FLAGS build --print-out-paths --no-link --impure ".#devenv.containers.\"$container\".dockerRun")
-      fi
-      ;;
-    search)
-      name=$1
-      shift
-      assemble
-      options=$(${nix}/bin/nix $NIX_FLAGS build --no-link --print-out-paths '.#optionsJSON' --impure)
-      results=$(${nix}/bin/nix $NIX_FLAGS search --json nixpkgs $name)
-      results_options=$(${coreutils}/bin/cat $options/share/doc/nixos/options.json | ${jq}/bin/jq "with_entries(select(.key | contains(\"$name\")))")
-      if [ "$results" = "{}" ]; then
-        echo "No packages found for '$name'."
-      else
-        ${jq}/bin/jq -r '[to_entries[] | {name: ("pkgs." + (.key | split(".") | del(.[0, 1]) | join("."))) } * (.value | { version, description})] | (.[0] |keys_unsorted | @tsv) , (["----", "-------", "-----------"] | @tsv), (.[]  |map(.) |@tsv)' <<< "$results" | ${util-linuxMinimal}/bin/column -ts $'\t'
-        echo
-      fi
-      echo
-      if [ "$results_options" = "{}" ]; then
-        echo "No options found for '$name'."
-      else
-        ${jq}/bin/jq -r '["option","type","default", "description"], ["------", "----", "-------", "-----------"],(to_entries[] | [.key, .value.type, .value.default, .value.description[0:80]]) | @tsv' <<< "$results_options" | ${util-linuxMinimal}/bin/column -ts $'\t'
-      fi
-      echo
-      echo "Found $(${jq}/bin/jq 'length' <<< "$results") packages and $(${jq}/bin/jq 'length' <<< "$results_options") options for '$name'."
-      ;;
-    init)
-      if [ "$#" -eq "1" ]
-      then
-        target="$1"
-        ${coreutils}/bin/mkdir -p "$target"
-        cd "$target"
-      fi
-
-      if [[ -f devenv.nix && -f devenv.yaml && -f .envrc ]]; then
-        echo "Aborting since devenv.nix, devenv.yaml and .envrc already exist."
-        exit 1
-      fi
-
-      # TODO: allow selecting which example and list them
-      example=simple
-
-      if [[ ! -f .envrc ]]; then
-        echo "Creating .envrc"
-        ${coreutils}/bin/cat ${examples}/$example/.envrc > .envrc
-      fi
-
-      if [[ ! -f devenv.nix ]]; then
-        echo "Creating devenv.nix"
-        ${coreutils}/bin/cat ${examples}/$example/devenv.nix > devenv.nix
-      fi
-
-      if [[ ! -f devenv.yaml ]]; then
-        echo "Creating devenv.yaml"
-        ${coreutils}/bin/cat ${examples}/$example/devenv.yaml > devenv.yaml
-      fi
-
-      if [[ ! -f .gitignore ]]; then
-        ${coreutils}/bin/touch .gitignore
-      fi
-
-      if ! ${gnugrep}/bin/grep -q "devenv" .gitignore; then
-        echo "Appending defaults to .gitignore"
-
-        echo "" >> .gitignore
-        echo "# Devenv" >> .gitignore
-        echo ".devenv*" >> .gitignore
-        echo "devenv.local.nix" >> .gitignore
-        echo "" >> .gitignore
-        echo "# direnv" >> .gitignore
-        echo ".direnv" >> .gitignore
-        echo "" >> .gitignore
-        echo "# pre-commit" >> .gitignore
-        echo ".pre-commit-config.yaml" >> .gitignore
-        echo "" >> .gitignore
-      fi
-      echo "Done."
-
-      if command -v direnv &> /dev/null; then
-        echo "direnv is installed. Running direnv allow."
-        direnv allow
-      fi
-      ;;
-    info)
-      assemble
-      ${nix}/bin/nix $NIX_FLAGS flake metadata | ${gnugrep}/bin/grep Inputs -A10000
-      echo
-      ${nix}/bin/nix $NIX_FLAGS eval --raw '.#info' --impure
-      ;;
-    update)
-      assemble
-      ${nix}/bin/nix $NIX_FLAGS flake update
-      ;;
-    version)
-      echo "devenv: ${version} ${pkgs.stdenv.system}"
-      ;;
-    ci)
-      assemble
-      ci=$(${nix}/bin/nix $NIX_FLAGS build --no-link --print-out-paths '.#ci' --impure)
-      add_gc ci $ci
-      ;;
-    gc)
-      SECONDS=0
-
-      for link in $(${findutils}/bin/find $GC_ROOT -type l); do
-        if [ ! -f $link ]; then
-          ${coreutils}/bin/unlink $link
-        fi
-      done
-
-      echo "Counting old devenvs ..."
-      echo
-      candidates=$(${findutils}/bin/find $GC_ROOT -type l)
-
-      before=$(${nix}/bin/nix $NIX_FLAGS path-info $candidates -r -S --json | ${jq}/bin/jq '[.[].closureSize | tonumber] | add')
-
-      echo "Found $(echo $candidates | ${coreutils}/bin/wc -l) environments of sum size $(( $before / 1024 / 1024 )) MB."
-      echo
-      echo "Garbage collecting ..."
-      echo
-      echo "Note: If you'd like this command to run much faster, leave a thumbs up at https://github.com/NixOS/nix/issues/7239"
-
-      ${nix}/bin/nix $NIX_FLAGS store delete --recursive $candidates
-
-      # after GC delete links again
-      for link in $(${findutils}/bin/find $GC_ROOT -type l); do
-        if [ ! -f $link ]; then
-          ${coreutils}/bin/unlink $link
-        fi
-      done
-
-      echo "Done in $SECONDS seconds."
-      ;;
-    *)
-      echo "https://devenv.sh (version ${version}): Fast, Declarative, Reproducible, and Composable Developer Environments"
-      echo
-      echo "Usage: devenv command [options] [arguments]"
-      echo
-      echo "Commands:"
-      echo
-      echo "init                      Scaffold devenv.yaml, devenv.nix, and .envrc inside the current directory."
-      echo "init TARGET               Scaffold devenv.yaml, devenv.nix, and .envrc inside TARGET directory."
-      echo "search NAME               Search packages matching NAME in nixpkgs input."
-      echo "shell                     Activate the developer environment."
-      echo "shell CMD [args]          Run CMD with ARGS in the developer environment. Useful when scripting."
-      echo "container [options] NAME  Generate a container for NAME. See devenv container --help and http://devenv.sh/containers"
-      echo "info                      Print information about the current developer environment."
-      echo "update                    Update devenv.lock from devenv.yaml inputs. See http://devenv.sh/inputs/#locking-and-updating-inputs"
-      echo "up                        Starts processes in foreground. See http://devenv.sh/processes"
-      echo "gc                        Removes old devenv generations. See http://devenv.sh/garbage-collection"
-      echo "ci                        Builds your developer environment and make sure all checks pass."
-      echo "version                   Display devenv version"
-      echo
-      exit 1
-  esac
-''
diff --git a/src/devenv/__init__.py b/src/devenv/__init__.py
new file mode 100644
index 000000000..b0161ddd7
--- /dev/null
+++ b/src/devenv/__init__.py
@@ -0,0 +1,4 @@
+import os
+
+if os.environ.get('DEVENV_DEVELOP') == "1":
+    os.environ["PATH"] = os.path.join(os.environ["DEVENV_NIX"], "bin") + ":" + os.environ["PATH"]
\ No newline at end of file
diff --git a/src/devenv/cli.py b/src/devenv/cli.py
new file mode 100644
index 000000000..919877237
--- /dev/null
+++ b/src/devenv/cli.py
@@ -0,0 +1,445 @@
+import os
+import shutil
+import subprocess
+import time
+import re
+from pathlib import Path
+import pkgutil
+import json
+
+import click
+import terminaltables
+
+
+from .yaml import validate_and_parse_yaml
+
+
+NIX_FLAGS = [
+    "--show-trace",
+    "--extra-experimental-features",
+    "\"nix-command flakes\"",
+    "--option",
+    "warn-dirty",
+    "false",
+]
+FILE = pkgutil.get_loader(__package__).load_module(__package__).__file__
+if 'site-packages' in FILE:
+    SRC_DIR = Path(FILE, '..', '..', 'src')
+else:
+    SRC_DIR = Path(FILE, '..', '..')
+MODULES_DIR = (SRC_DIR / 'modules').resolve()
+FLAKE_FILE_TEMPL = os.path.join(MODULES_DIR, "flake.tmpl.nix")
+FLAKE_FILE = ".devenv.flake.nix"
+FLAKE_LOCK = "devenv.lock"
+
+# define system like x86_64-linux
+SYSTEM = os.uname().machine.lower().replace("arm", "aarch") + "-" + os.uname().sysname.lower()
+
+def run_nix(command: str) -> str:
+    ctx = click.get_current_context()
+    nix_flags = ctx.obj['nix_flags']
+    flags = " ".join(NIX_FLAGS) + " " + " ".join(nix_flags)
+    command_flags = " ".join(ctx.obj['command_flags'])
+    return run_command(f"nix {flags} {command} {command_flags}")
+
+def run_command(command: str) -> str:
+    if command.startswith("nix"):
+        if os.environ.get("DEVENV_NIX"):
+            nix = os.path.join(os.environ["DEVENV_NIX"], "bin")
+            command = f"{nix}/{command}"
+        else:
+            click.echo("$DEVENV_NIX is not set, but required as devenv doesn't work without a few Nix patches.", err=True)
+            click.echo("Please follow https://devenv.sh/getting-started/ to install devenv.", err=True)
+            exit(1)
+    try:
+        return subprocess.run(
+            command, 
+            shell=True,
+            check=True, 
+            env=os.environ.copy(),
+            stdout=subprocess.PIPE,
+            universal_newlines=True).stdout.strip()
+    except subprocess.CalledProcessError as e:
+        click.echo(f"Error: Following command exited with code {e.returncode}:\n\n  {e.cmd}", err=True)
+        exit(e.returncode)
+
+CONTEXT_SETTINGS = dict(max_content_width=120)
+
+@click.group(context_settings=CONTEXT_SETTINGS)
+@click.option(
+    '--nix-flags', '-n', 
+    help='Flags to pass to Nix. See `man nix.conf 5`. Example: --nix-flags "--option bash-prompt >"',
+    metavar="NIX-FLAGS",
+    multiple=True)
+@click.option(
+    '--debugger', '-d',
+    help='Enable Nix debugger.',
+    is_flag=True)
+@click.option(
+    '--system', '-s',
+    help='Nix system to use.',
+    default=SYSTEM)
+@click.option(
+    '--offline', '-o',
+    help='Disable network access.',
+    is_flag=True)
+@click.option(
+    '--disable-eval-cache',
+    help='Disable Nix evaluation cache.',
+    is_flag=True)
+@click.pass_context
+def cli(ctx, disable_eval_cache, offline, system, debugger, nix_flags):
+    """https://devenv.sh: Fast, Declarative, Reproducible, and Composable Developer Environments."""
+    ctx.ensure_object(dict)
+    ctx.obj['system'] = system
+    ctx.obj['command_flags'] = []
+    ctx.obj['nix_flags'] = list(nix_flags)
+    ctx.obj['nix_flags'] += ['--system', system]
+    if offline:
+        ctx.obj['nix_flags'] += ['--offline']
+    if debugger:
+        # ignore-try is needed to avoid catching unrelated errors
+        ctx.obj['command_flags'] += ['--debugger', '--ignore-try']
+    if disable_eval_cache:
+        ctx.obj['nix_flags'] += ['--option', 'eval-cache', 'false']
+
+    if 'XDG_DATA_HOME' not in os.environ:
+        ctx.obj['gc_root'] = os.path.join(os.environ['HOME'], '.devenv', 'gc')
+    else:
+        ctx.obj['gc_root'] = os.path.join(os.environ['XDG_DATA_HOME'], 'devenv', 'gc')
+    
+    Path(ctx.obj['gc_root']).mkdir(parents=True, exist_ok=True)
+    ctx.obj['gc_project'] = os.path.join(ctx.obj['gc_root'], str(int(time.time() * 1000)))
+
+
+def add_gc(name, store_path):
+    """Register a GC root"""
+    ctx = click.get_current_context()
+    run_command(f'nix-store --add-root "{os.environ["DEVENV_GC"]}/{name}" -r {store_path} >/dev/null')
+    os.symlink(store_path, f'{ctx.obj["gc_project"]}-{name}', True)
+
+
+@cli.command(hidden=True)
+@click.pass_context
+def assemble(ctx):
+    if not os.path.exists('devenv.nix'):
+        click.echo('File devenv.nix does not exist. To get started, run:')
+        click.echo('  $ devenv init')
+        exit(1)
+
+    DEVENV_DIR = Path(os.getcwd()) / '.devenv'
+    os.environ['DEVENV_DIR'] = str(DEVENV_DIR)
+    DEVENV_GC = DEVENV_DIR / 'gc'
+    os.environ['DEVENV_GC'] = str(DEVENV_GC)
+    DEVENV_GC.mkdir(parents=True, exist_ok=True)
+
+    if os.path.exists('devenv.yaml'):
+        validate_and_parse_yaml(DEVENV_DIR)
+    else:
+        for file in ['devenv.json', 'flake.json', 'imports.txt']:
+            file_path = DEVENV_DIR / file
+            if file_path.exists():
+                os.remove(file_path)
+
+    with open(FLAKE_FILE_TEMPL) as f:
+        flake = f.read()
+        system = ctx.obj['system']
+
+        with open(FLAKE_FILE, 'w') as f:
+            devenv_vars = (f"""
+  version = "{get_version()}";
+  system = "{system}";
+  devenv_root = "{os.getcwd()}";
+            """)
+            # replace __DEVENV_VARS__ in flake using regex
+            flake = re.sub(r'__DEVENV_VARS__', devenv_vars, flake)
+            f.write(flake)
+
+
+@cli.command(
+    help="Deletes previous devenv generations. See http://devenv.sh/garbage-collection",
+    short_help="Deletes previous devenv generations. See http://devenv.sh/garbage-collection",
+)
+@click.pass_context
+def gc(ctx):
+    GC_ROOTS = ctx.obj['gc_root']
+    start = time.time()
+
+    # remove dangling symlinks
+    click.echo(f'Removing non-existings symlinks in {GC_ROOTS} ...')
+    to_gc, removed_symlinks = cleanup_symlinks(GC_ROOTS)
+
+    click.echo(f'  Found {len(to_gc)} active symlinks.')
+    click.echo(f'  Deleted {len(removed_symlinks)} dangling symlinks.')
+    click.echo()
+
+    click.echo(f'Running garbage collection (this process may take some time) ...')
+    # TODO: ideally nix would report some statistics about the GC as JSON
+    run_nix(f'store delete --recursive {" ".join(to_gc)}')
+
+    after_gc, removed_symlinks = cleanup_symlinks(GC_ROOTS)
+    end = time.time()
+
+    click.echo()
+    click.echo(f'Done. Successfully removed {len(to_gc) - len(after_gc)} symlinks in {end - start:.0f} seconds.')
+
+def cleanup_symlinks(folder):
+    to_gc = []
+    removed_symlinks = []
+    for root, dirs, files in os.walk(folder):
+        for name in files:
+            full_path = os.path.join(root, name)
+            if os.path.islink(full_path):
+                if not os.path.isfile(full_path):
+                    os.unlink(full_path)
+                    removed_symlinks.append(full_path)
+                else:
+                    to_gc.append(full_path)
+    return to_gc, removed_symlinks
+
+def get_dev_environment(ctx, is_shell=False):
+    ctx.invoke(assemble)
+    if is_shell:
+        click.echo('Building shell ...')
+    gc_root = os.path.join(os.environ['DEVENV_GC'], 'shell')
+    env = run_nix(f"print-dev-env --impure --profile {gc_root}")
+    run_command('nix-env -p {gc_root} --delete-generations old')
+    symlink_force(Path(f'{ctx.obj["gc_project"]}-shell'), gc_root)
+    return env, gc_root
+
+@cli.command(
+    help="Activate the developer environment.",
+    short_help="Activate the developer environment.",
+    context_settings=dict(
+        ignore_unknown_options=True,
+    )
+)
+@click.argument('extra_args', nargs=-1, type=click.UNPROCESSED)
+@click.argument('cmd', required=False)
+@click.pass_context
+def shell(ctx, cmd, extra_args):
+    env, gc_root = get_dev_environment(ctx, is_shell=True)
+    if cmd:
+        run_nix(f"develop {gc_root} -c {cmd} " + "".join(extra_args))
+    else:
+        click.echo('Entering shell ...')
+        run_nix(f"develop {gc_root}")
+        
+def symlink_force(src, dst):
+    src.unlink(missing_ok=True)
+    Path(src).symlink_to(dst)
+
+@cli.command(
+    help="Starts processes in foreground. See http://devenv.sh/processes", 
+    short_help="Starts processes in foreground. See http://devenv.sh/processes",
+)
+@click.argument('command', required=False)
+@click.pass_context
+def up(ctx, command):
+    click.echo('⌛ Building processes')
+    ctx.invoke(assemble)
+    procfilescript = run_nix(f"build --no-link --print-out-paths --impure '.#procfileScript'")
+    with open(procfilescript, 'r') as file:
+        contents = file.read().strip()
+    if contents == '':
+        click.echo("❌ No 'processes' option defined: https://devenv.sh/processes/")
+        exit(1)
+    else:
+        click.echo('🚀 Starting processes')
+        add_gc('procfilescript', procfilescript)
+        # TODO: print output to stdout
+        #run_command(procfilescript + ' ' + (command or ''))
+        args = [] if not command else [command]
+        subprocess.run([procfilescript] + args)
+
+@cli.command()
+@click.argument('name')
+@click.pass_context
+def search(ctx, name):
+    """Search packages matching NAME in nixpkgs input."""
+    ctx.invoke(assemble)
+    options = run_nix(f"build --no-link --print-out-paths '.#optionsJSON' --impure")
+    search = run_nix(f"search --json nixpkgs {name}")
+
+    with open(Path(options) / 'share' / 'doc' / 'nixos' / 'options.json') as f:
+        options_results = []
+        for key, value in json.load(f).items():
+            if name in key:
+                options_results.append((
+                    key,
+                    value['type'],
+                    value['default'],
+                    value['description'][:80]
+                ))
+        results_options_count = len(options_results)
+
+    search_results = []
+    for key, value in json.loads(search).items():
+        search_results.append(
+            (".".join(key.split('.')[2:])
+            , value['version']
+            , value['description'][:80]
+            )
+        )
+    search_results_count = len(search_results)
+
+    if search_results:
+        click.echo(
+            terminaltables.AsciiTable(
+                [("Package", "Version", "Description")] 
+                + search_results
+            ).table
+        )
+
+    if options_results:
+        click.echo(
+            terminaltables.AsciiTable(
+                [("Option", "Type", "Default", "Description")] 
+                + options_results
+            ).table
+        )
+    
+    click.echo(f"Found {search_results_count} packages and {results_options_count} options for '{name}'.")
+
+@cli.command(
+    help="Build, copy and run a container. See http://devenv.sh/containers",
+    short_help="Build, copy and run a container. See http://devenv.sh/containers",
+)
+@click.option('--registry', default=None, help='Registry to copy the container to.', metavar="REGISTRY")
+@click.option('--copy', is_flag=True, help='Copy the container to the registry.')
+@click.option('--copy-args', default=None, help='Arguments passed to `skopeo copy`.', metavar="ARGS")
+@click.option('--docker-run', is_flag=True, help='Execute `docker run`.')
+@click.argument('container_name')
+@click.pass_context
+def container(ctx, registry, copy, copy_args, docker_run, container_name):
+    ctx.invoke(assemble)
+
+    # build container
+    os.environ['DEVENV_CONTAINER'] = container_name
+    # NOTE: we need --impure here to read DEVENV_CONTAINER
+    spec = run_nix(f"build --impure --print-out-paths --no-link .#devenv.containers.\"{container_name}\".derivation")
+    print(spec)
+  
+    # copy container
+    if copy or docker_run:
+        copy_script = run_nix(f"build --print-out-paths --no-link \
+          --impure .#devenv.containers.\"{container_name}\".copyScript")
+        
+        if docker_run:
+            registry = "docker-daemon:"
+        
+        subprocess.run(
+            f"{copy_script} {spec} {registry} {copy_args or ''}",
+            shell=True,
+            check=True)
+
+    if docker_run:
+        docker_script = run_nix(f"build --print-out-paths --no-link --impure \
+          .#devenv.containers.\"{container_name}\".dockerRun")
+        
+        click.echo()
+        click.echo(f"Starting {container_name} container ...")
+        click.echo(docker_script)
+        subprocess.run(docker_script)
+
+@cli.command(
+    help="Print information about this developer environment.",
+    short_help="Print information about this developer environment.",
+)
+@click.pass_context
+def info(ctx):
+    ctx.invoke(assemble)
+    # TODO: use --json and reconstruct input metadata
+    metadata = run_nix("flake metadata")
+    matches = re.search(r"(Inputs:.+)$", metadata, re.DOTALL)
+    if matches:
+        inputs = matches.group(1)
+    else:
+        inputs = ""
+    info_ = run_nix("eval --raw '.#info' --impure")
+    print(f"{inputs}\n{info_}")
+
+@cli.command()
+@click.pass_context
+def version(ctx):
+    """Display devenv version."""
+    version = get_version()
+    print(f"devenv {version} ({ctx.obj['system']})")
+
+@cli.command(
+    help="Scaffold devenv.yaml, devenv.nix, and .envrc.",
+    short_help="Scaffold devenv.yaml, devenv.nix, and .envrc.",
+)
+@click.argument('target', default='.')
+def init(target):
+    os.makedirs(target, exist_ok=True)
+
+    required_files = ['devenv.nix', 'devenv.yaml', '.envrc']
+    for filename in required_files:
+        if os.path.exists(Path(target, filename)):
+            print(f"Aborting since {filename} already exist.")
+            exit(1)
+            return
+
+    example = "simple"
+    examples_path = Path(MODULES_DIR / ".." / ".." / "examples").resolve()
+
+    for filename in required_files:
+        full_filename = Path(target, filename)
+        if not os.path.exists(full_filename):
+            print(f"Creating {full_filename}")
+            shutil.copyfile(os.path.join(examples_path, example, filename), full_filename)
+
+    with open('.gitignore', 'a+') as gitignore_file:
+        if 'devenv' not in gitignore_file.read():
+            print("Appending defaults to .gitignore")
+            gitignore_file.write("\n")
+            gitignore_file.write("# Devenv\n")
+            gitignore_file.write(".devenv*\n")
+            gitignore_file.write("devenv.local.nix\n")
+            gitignore_file.write("\n")
+            gitignore_file.write("# direnv\n")
+            gitignore_file.write(".direnv\n")
+            gitignore_file.write("\n")
+            gitignore_file.write("# pre-commit\n")
+            gitignore_file.write(".pre-commit-config.yaml\n")
+            gitignore_file.write("\n")
+
+    print("Done.")
+
+    # Check if direnv is installed
+    if shutil.which('direnv'):
+        print("direnv is installed. Running $ direnv allow .")
+        subprocess.run(['direnv', 'allow'])
+
+@cli.command(
+    help="Update devenv.lock from devenv.yaml inputs. See http://devenv.sh/inputs/",
+    short_help="Update devenv.lock from devenv.yaml inputs. See http://devenv.sh/inputs/",
+)
+@click.argument('input_name', required=False)
+@click.pass_context
+def update(ctx, input_name):
+    ctx.invoke(assemble)
+
+    if input_name:
+        run_nix(f"flake lock --update-input {input_name}")
+    else:
+        run_nix(f"flake update")
+
+@cli.command()
+@click.pass_context
+def ci(ctx):
+    """Builds your developer environment and checks if everything builds."""
+    ctx.invoke(assemble)
+    output_path = run_nix(f"build --no-link --print-out-paths --impure .#ci")
+    add_gc('ci', output_path)
+
+@cli.command()
+@click.pass_context
+def print_dev_env(ctx):
+    env, _ = get_dev_environment(ctx)
+    click.echo(env)
+
+def get_version():
+    with open(Path(MODULES_DIR, "latest-version")) as f:
+        return f.read().strip()
diff --git a/src/devenv/yaml.py b/src/devenv/yaml.py
new file mode 100644
index 000000000..b43582baa
--- /dev/null
+++ b/src/devenv/yaml.py
@@ -0,0 +1,44 @@
+import json
+import sys
+import os
+from pathlib import Path
+
+from strictyaml import Map, MapPattern, Str, Seq
+from strictyaml import load, Bool, Any, Optional, YAMLError
+
+
+inputsSchema = MapPattern(Str(), Map({
+      "url": Str(),
+      Optional("flake", default=None): Bool(),
+      Optional("inputs", default=None): Any(),
+      Optional("overlays", default=None): Seq(Str())
+}))
+
+schema = Map({
+      Optional("inputs", default=None): inputsSchema,
+      Optional("allowUnfree", default=False): Bool(),
+      Optional("imports", default=None): Seq(Str()),
+      Optional("permittedInsecurePackages", default=None): Seq(Str())
+})
+
+def validate_and_parse_yaml(dot_devenv_root):
+  try:
+      with open(Path("devenv.yaml")) as f:
+          devenv = load(f.read(), schema, label="devenv.yaml").data
+  except YAMLError as error:
+      print("Validation error in `devenv.yaml`", error)
+      sys.exit(1)
+
+  inputs = {}
+  for input, attrs in devenv.get('inputs', {}).items():
+      inputs[input] = {k: attrs[k] for k in ('url', 'inputs', 'flake')
+                       if k in attrs}
+
+  with open(os.path.join(dot_devenv_root, "flake.json"), 'w') as f:
+      f.write(json.dumps(inputs))
+
+  with open(os.path.join(dot_devenv_root, "devenv.json"), 'w') as f:
+      f.write(json.dumps(devenv))
+
+  with open(os.path.join(dot_devenv_root, "imports.txt"), 'w') as f:
+      f.write("\n".join(devenv.get('imports', [])))
\ No newline at end of file
diff --git a/src/flake.nix b/src/flake.nix
deleted file mode 100644
index 970585903..000000000
--- a/src/flake.nix
+++ /dev/null
@@ -1,85 +0,0 @@
-{ pkgs, version }: pkgs.writeText "devenv-flake" ''
-  {
-    inputs = {
-      pre-commit-hooks.url = "github:cachix/pre-commit-hooks.nix";
-      pre-commit-hooks.inputs.nixpkgs.follows = "nixpkgs";
-      nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
-      devenv.url = "github:cachix/devenv?dir=src/modules";
-    } // (if builtins.pathExists ./.devenv/flake.json 
-         then builtins.fromJSON (builtins.readFile ./.devenv/flake.json)
-         else {});
-
-    outputs = { nixpkgs, ... }@inputs:
-      let
-        devenv = if builtins.pathExists ./.devenv/devenv.json
-          then builtins.fromJSON (builtins.readFile ./.devenv/devenv.json)
-          else {};
-        getOverlays = inputName: inputAttrs:
-          map (overlay: let
-              input = inputs.''${inputName} or (throw "No such input `''${inputName}` while trying to configure overlays.");
-            in input.overlays.''${overlay} or (throw "Input `''${inputName}` has no overlay called `''${overlay}`. Supported overlays: ''${nixpkgs.lib.concatStringsSep ", " (builtins.attrNames input.overlays)}"))
-            inputAttrs.overlays or [];
-        overlays = nixpkgs.lib.flatten (nixpkgs.lib.mapAttrsToList getOverlays (devenv.inputs or {}));
-        pkgs = import nixpkgs {
-          system = "${pkgs.stdenv.system}";
-          config = {
-            allowUnfree = devenv.allowUnfree or false;
-            permittedInsecurePackages = devenv.permittedInsecurePackages or [];
-          };
-          inherit overlays;
-        };
-        lib = pkgs.lib;
-        importModule = path:
-          if lib.hasPrefix "./" path
-          then if lib.hasSuffix ".nix" path
-               then ./. + (builtins.substring 1 255 path)
-               else ./. + (builtins.substring 1 255 path) + "/devenv.nix"
-          else if lib.hasPrefix "../" path 
-              then throw "devenv: ../ is not supported for imports"
-              else let
-                paths = lib.splitString "/" path;
-                name = builtins.head paths;
-                input = inputs.''${name} or (throw "Unknown input ''${name}");
-                subpath = "/''${lib.concatStringsSep "/" (builtins.tail paths)}";
-                devenvpath = "''${input}" + subpath + "/devenv.nix";
-                in if builtins.pathExists devenvpath
-                  then devenvpath
-                  else throw (devenvpath + " file does not exist for input ''${name}.");
-        project = pkgs.lib.evalModules {
-          specialArgs = inputs // { inherit inputs pkgs; };
-          modules = [
-            (inputs.devenv.modules + /top-level.nix)
-            { devenv.cliVersion = "${version}"; }
-          ] ++ (map importModule (devenv.imports or [])) ++ [
-            ./devenv.nix
-            (devenv.devenv or {})
-            (if builtins.pathExists ./devenv.local.nix then ./devenv.local.nix else {})
-          ];
-        };
-        config = project.config;
-
-        options = pkgs.nixosOptionsDoc {
-          options = builtins.removeAttrs project.options [ "_module" ];
-          # Unpack Nix types, e.g. literalExpression, mDoc.
-          transformOptions =
-            let isDocType = v: builtins.elem v [ "literalDocBook" "literalExpression" "literalMD" "mdDoc" ];
-            in lib.attrsets.mapAttrs (_: v:
-              if v ? _type && isDocType v._type then
-                v.text
-              else if v ? _type && v._type == "derivation" then
-                v.name
-              else
-                v
-            );
-        };
-      in {
-        packages."${pkgs.stdenv.system}" = {
-          optionsJSON = options.optionsJSON;
-          inherit (config) info procfileScript procfileEnv procfile;
-          ci = config.ciDerivation;
-        };
-        devenv.containers = config.containers;
-        devShell."${pkgs.stdenv.system}" = config.shell;
-      };
-  }
-''
diff --git a/src/modules/flake.tmpl.nix b/src/modules/flake.tmpl.nix
new file mode 100644
index 000000000..cbb8ac88c
--- /dev/null
+++ b/src/modules/flake.tmpl.nix
@@ -0,0 +1,91 @@
+{
+  inputs = {
+    pre-commit-hooks.url = "github:cachix/pre-commit-hooks.nix";
+    pre-commit-hooks.inputs.nixpkgs.follows = "nixpkgs";
+    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
+    devenv.url = "github:cachix/devenv?dir=src/modules";
+  } // (if builtins.pathExists ./.devenv/flake.json
+  then builtins.fromJSON (builtins.readFile ./.devenv/flake.json)
+  else { });
+
+  outputs = { nixpkgs, ... }@inputs:
+    let
+      __DEVENV_VARS__
+        devenv =
+        if builtins.pathExists ./.devenv/devenv.json
+        then builtins.fromJSON (builtins.readFile ./.devenv/devenv.json)
+        else { };
+      getOverlays = inputName: inputAttrs:
+        map
+          (overlay:
+            let
+              input = inputs.${inputName} or (throw "No such input `${inputName}` while trying to configure overlays.");
+            in
+              input.overlays.${overlay} or (throw "Input `${inputName}` has no overlay called `${overlay}`. Supported overlays: ${nixpkgs.lib.concatStringsSep ", " (builtins.attrNames input.overlays)}"))
+          inputAttrs.overlays or [ ];
+      overlays = nixpkgs.lib.flatten (nixpkgs.lib.mapAttrsToList getOverlays (devenv.inputs or { }));
+      pkgs = import nixpkgs {
+        inherit system;
+        config = {
+          allowUnfree = devenv.allowUnfree or false;
+          permittedInsecurePackages = devenv.permittedInsecurePackages or [ ];
+        };
+        inherit overlays;
+      };
+      lib = pkgs.lib;
+      importModule = path:
+        if lib.hasPrefix "./" path
+        then if lib.hasSuffix ".nix" path
+        then ./. + (builtins.substring 1 255 path)
+        else ./. + (builtins.substring 1 255 path) + "/devenv.nix"
+        else if lib.hasPrefix "../" path
+        then throw "devenv: ../ is not supported for imports"
+        else
+          let
+            paths = lib.splitString "/" path;
+            name = builtins.head paths;
+            input = inputs.${name} or (throw "Unknown input ${name}");
+            subpath = "/${lib.concatStringsSep "/" (builtins.tail paths)}";
+            devenvpath = "${input}" + subpath + "/devenv.nix";
+          in
+          if builtins.pathExists devenvpath
+          then devenvpath
+          else throw (devenvpath + " file does not exist for input ${name}.");
+      project = pkgs.lib.evalModules {
+        specialArgs = inputs // { inherit inputs pkgs; };
+        modules = [
+          (inputs.devenv.modules + /top-level.nix)
+          { devenv.cliVersion = "${version}"; }
+        ] ++ (map importModule (devenv.imports or [ ])) ++ [
+          ./devenv.nix
+          (devenv.devenv or { })
+          (if builtins.pathExists ./devenv.local.nix then ./devenv.local.nix else { })
+        ];
+      };
+      config = project.config;
+
+      options = pkgs.nixosOptionsDoc {
+        options = builtins.removeAttrs project.options [ "_module" ];
+        # Unpack Nix types, e.g. literalExpression, mDoc.
+        transformOptions =
+          let isDocType = v: builtins.elem v [ "literalDocBook" "literalExpression" "literalMD" "mdDoc" ];
+          in lib.attrsets.mapAttrs (_: v:
+            if v ? _type && isDocType v._type then
+              v.text
+            else if v ? _type && v._type == "derivation" then
+              v.name
+            else
+              v
+          );
+      };
+    in
+    {
+      packages."${system}" = {
+        optionsJSON = options.optionsJSON;
+        inherit (config) info procfileScript procfileEnv procfile;
+        ci = config.ciDerivation;
+      };
+      devenv.containers = config.containers;
+      devShell."${system}" = config.shell;
+    };
+}