[
  {
    "path": ".envrc",
    "content": "source ./doc/om/develop/omnixrc/v1\n\n# We don't watch on `flake/*.nix` to avoid changes in modules not relevant to devShell triggering reload\nwatch_file \\\n    ./doc/om/develop/omnixrc/v1 \\\n    nix/modules/flake/nixpkgs.nix \\\n    nix/modules/flake/rust.nix \\\n    nix/modules/flake/devshell.nix \\\n    nix/envs/default.nix \\\n    ./crates/*/crate.nix \\\n    *.nix \\\n    om.yaml \\\n    rust-toolchain.toml \\\n    crates/omnix-init/registry/flake.*\n\n# Dogfood our own ./omnixrc!\nuse omnix\n"
  },
  {
    "path": ".gitattributes",
    "content": "flake.lock linguist-generated=true\ncrates/omnix-gui/assets/tailwind.css linguist-generated=true\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: \"CI\"\non:\n  push:\n    branches:\n      - \"main\"\n      - \"ci/**\"\n  pull_request:\n\njobs:\n  website:\n    if: github.ref == 'refs/heads/main'\n    needs: main\n    uses: ./.github/workflows/website.yaml\n    with:\n      static-site-path: ${{ needs.main.outputs.OMWEBSITE }}\n    secrets: inherit\n\n  main:\n    runs-on: ${{ matrix.system }}\n    permissions:\n      contents: read\n    outputs:\n      # It is important to match the matrix.system here\n      # With that of website.yaml\n      OMWEBSITE: ${{ steps.omci.outputs.OMWEBSITE_x86_64-linux }}\n    strategy:\n      matrix:\n        system: [x86_64-linux, aarch64-linux, aarch64-darwin, x86_64-darwin]\n        isMain:\n          - ${{ contains(github.ref, 'main') }}\n        # Excluded emulated builds on PRs\n        exclude:\n          - system: aarch64-linux\n            isMain: false\n          - system: x86_64-darwin\n            isMain: false\n      fail-fast: false\n    steps:\n      - uses: actions/checkout@v4\n\n      # Build omnix first, so we can use it to build the rest of the flake outputs.\n      # This also separates the CI log for both these obviously distinct steps.\n      - name: Build Omnix package\n        run: nix build --no-link --print-out-paths --accept-flake-config\n\n      # Build flake outputs\n      # Run omnix using self.\n      - name: Omnix CI\n        run: |\n          nix --accept-flake-config run . -- ci run \\\n            --extra-access-tokens ${{ secrets.GITHUB_TOKEN }} \\\n            --systems \"${{ matrix.system }}\" \\\n            --results=$HOME/omci.json \\\n            -- --accept-flake-config\n\n      - name: Omnix results\n        id: omci\n        run: |\n          cat $HOME/omci.json | jq\n\n          # Retrieve the store path for the given package out of the given subflake.\n          get_output() {\n            subflake=$1 name=$2 \\\n            jq -r '.result.[$ENV.subflake].build.byName.[$ENV.name]' < $HOME/omci.json\n          }\n\n          echo \"OMCIJSON_PATH=$HOME/omci.json\" >> \"$GITHUB_OUTPUT\"\n          echo \"OMCIJSON=$(cat $HOME/omci.json)\" >> \"$GITHUB_OUTPUT\"\n          echo \"OMPACKAGE=$(get_output omnix omnix-cli)\" >> \"$GITHUB_OUTPUT\"\n          echo \"OMWEBSITE_${{ matrix.system }}=$(get_output doc emanote-static-website-default)\" >> \"$GITHUB_OUTPUT\"\n\n      - name: \"Omnix: Upload results\"\n        uses: actions/upload-artifact@v4\n        with:\n          name: omci-${{ matrix.system }}.json\n          path: ${{ steps.omci.outputs.OMCIJSON_PATH }}\n          if-no-files-found: error\n\n      # Login to the Attic with the token that allows pushing Nix store objects to the cache\n      - name: Attic login\n        if: github.ref == 'refs/heads/main'\n        run: attic login chutney https://cache.nixos.asia ${{ secrets.ATTIC_LOGIN_TOKEN }}\n\n      # Push the Nix cache\n      - name: Push to attic\n        if: github.ref == 'refs/heads/main'\n        run: attic push chutney:oss $HOME/omci.json\n\n"
  },
  {
    "path": ".github/workflows/website.yaml",
    "content": "name: Website Deploy\n\non:\n  workflow_call:\n    inputs:\n      static-site-path:\n        type: string\n        required: true\n        description: \"Path to the static site to deploy\"\n\njobs:\n  upload:\n    runs-on: x86_64-linux\n    steps:\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v3\n        with:\n          path: ${{ inputs.static-site-path }}\n  deploy:\n    runs-on: x86_64-linux\n    needs: upload\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages\n    permissions:\n      contents: read\n      pages: write\n      id-token: write\n    # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.\n    # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.\n    concurrency:\n      group: \"pages\"\n      cancel-in-progress: false\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\nresult\nresult-lib\ndist\n.direnv\n\n/assets/tailwind.css\n\n/.vscode/spellright.dict\n\n/.pre-commit-config.yaml\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n    \"recommendations\": [\n        \"rust-lang.rust-analyzer\",\n        \"mkhl.direnv\",\n        \"jnoortheen.nix-ide\",\n        \"bradlc.vscode-tailwindcss\",\n        \"tamasfe.even-better-toml\",\n        \"fill-labs.dependi\"\n    ]\n}"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"nixEnvSelector.nixFile\": \"${workspaceRoot}/shell.nix\",\n    \"rust-analyzer.cargo.features\": \"all\",\n    \"rust-analyzer.check.command\": \"clippy\",\n    \"editor.formatOnSave\": true,\n    // https://twitter.com/sridca/status/1674947342607216641\n    \"editor.inlayHints.enabled\": \"offUnlessPressed\",\n    \"emmet.includeLanguages\": {\n        \"rust\": \"html\",\n        \"*.rs\": \"html\"\n    },\n    \"tailwindCSS.includeLanguages\": {\n        \"rust\": \"html\",\n        \"*.rs\": \"html\"\n    },\n    \"tailwindCSS.experimental.classRegex\": [\n        \"class: \\\"(.*)\\\"\"\n    ],\n    \"files.associations\": {\n        \"*.css\": \"tailwindcss\"\n    }\n}"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nresolver = \"2\"\n\nmembers = [\n  \"crates/omnix-common\",\n  \"crates/omnix-cli\",\n  \"crates/omnix-init\",\n  \"crates/omnix-develop\",\n  \"crates/nix_rs\",\n  \"crates/omnix-ci\",\n  \"crates/omnix-health\",\n]\n\n[workspace.dependencies]\nanyhow = \"1.0.75\"\nasync-walkdir = \"2.0.0\"\nbytesize = { version = \"1.3.0\", features = [\"serde\"] }\ncfg-if = \"1\"\nclap = { version = \"4.3\", features = [\"derive\", \"env\"] }\nclap-verbosity-flag = \"2.2.0\"\ncolored = { version = \"2.0\" }\nconsole = \"0.15.8\"\nconsole_error_panic_hook = \"0.1\"\nconsole_log = \"1\"\ndirenv = \"0.1.1\"\nfermi = \"0.4.3\"\nfutures-lite = \"2.3.0\"\nglob = \"0.3.1\"\nglobset = { version = \"0.4\", features = [\"serde1\"] }\nhttp = \"0.2\"\nhuman-panic = \"1.1.5\"\ninquire = \"0.7.5\"\nitertools = \"0.13\"\nis_proc_translated = { version = \"0.1.1\" }\nlazy_static = \"1.4.0\"\npulldown-cmark-mdcat = \"2.5.0\"\npulldown-cmark = { version = \"0.12.1\", default-features = false }\nnix_rs = { version = \"1.0.0\", path = \"./crates/nix_rs\" }\nnonempty = { version = \"0.10.0\", features = [\"serialize\"] }\nomnix-ci = { version = \"1.0.0\", path = \"./crates/omnix-ci\" }\nomnix-common = { version = \"1.0.0\", path = \"./crates/omnix-common\" }\nomnix-develop = { version = \"1.0.0\", path = \"./crates/omnix-develop\" }\nomnix-health = { version = \"1.0.0\", path = \"./crates/omnix-health\" }\nomnix-init = { version = \"1.0.0\", path = \"./crates/omnix-init\" }\nos_info = \"3.7.0\"\nreqwest = { version = \"0.11\", features = [\"blocking\", \"json\"] }\nregex = \"1.9.3\"\nsemver = { version = \"1.0.22\", features = [\"serde\"] }\nserde = { version = \"1.0.197\", features = [\"derive\"] }\nserde_qs = \"0.13.0\"\nserde_json = \"1.0\"\nserde_repr = \"0.1.18\"\nserde_with = { version = \"3.2\", features = [\"json\"] }\nserde_yaml = \"0.9\"\nshell-words = { version = \"1.1.0\" }\nsysinfo = \"0.29.10\"\nsyntect = { version = \"5.3.0\", features = [\"default-syntaxes\"] }\ntabled = \"0.15\"\ntempfile = \"3\"\ntermimad = \"0.30.0\"\nthiserror = \"1.0\"\ntokio = { version = \"1.43.1\", features = [\"full\"] }\ntracing = \"0.1\"\ntracing-subscriber = { version = \"0.3\", features = [\"env-filter\"] }\ntry-guard = \"0.2.0\"\nurl = { version = \"2.4\", features = [\"serde\"] }\nurlencoding = \"2.1.3\"\nuuid = { version = \"1.3.0\", features = [\"serde\", \"v4\", \"js\"] }\nwhich = { version = \"4.4.2\" }\nclap_complete = \"4.5.0\"\nclap_complete_nushell = \"4.5\"\nwhoami = \"1.5.2\"\n\n[profile.release]\nstrip = true    # Automatically strip symbols from the binary.\nopt-level = \"z\" # Optimize for size.\nlto = true\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "[![project chat](https://img.shields.io/github/discussions/juspay/omnix)](https://github.com/juspay/omnix/discussions)\n[![Naiveté Compass of Mood](https://img.shields.io/badge/naïve-FF10F0)](https://compass.naivete.me/ \"This project follows the 'Naiveté Compass of Mood'\")\n\n# omnix\n\n<img width=\"10%\" src=\"./doc/favicon.svg\">\n\n*Pronounced [`/ɒmˈnɪks/`](https://ipa-reader.com/?text=%C9%92m%CB%88n%C9%AAks&voice=Geraint)*\n\nOmnix aims to supplement the [Nix](https://nixos.asia/en/nix) CLI to improve developer experience.\n\n## Usage\n\nSee <https://omnix.page/>\n\n## Developing\n\n1. [Install Nix](https://nixos.asia/en/install)\n1. [Setup `direnv`](https://nixos.asia/en/direnv)\n1. Clone this repo, `cd` to it, and run `direnv allow`.\n\nThis will automatically activate the nix develop shell. Open VSCode and install recommended extensions, ensuring that direnv activates in VSCode as well.\n\n### Running locally\n\nTo run `omnix-cli`,\n\n```sh\njust watch # Or `just w`; you can also pass args, e.g.: `just w show`\n```\n\n### Nix workflows\n\nInside the nix develop shell (activated by direnv) you can use any of the `cargo` or `rustc` commands, as well as [`just`](https://just.systems/) workflows. Nix specific commands can also be used to work with the project:\n\n```sh\n# Full nix build of CLI\nnix build\n\n# Build and run the CLI\nnix run\n```\n\n### Contributing\n\n>[!TIP]\n> Run `just pca` to autoformat the source tree.\n\n- Run `just ci` to **run CI locally**.\n- Add **documentation** wherever useful.\n    - Run `just doc run` to preview website docs; edit, and run `just doc check`\n    - To preview Rust API docs, run `just doc cargo`.\n- Changes must accompany a corresponding `history.md` entry.[^cc]\n\n[^cc]: We don't use any automatic changelog generator for this repo.\n\n### Release HOWTO\n\nBegin with a release PR:\n\n- Pick a version\n- Update `history.md` to make sure new release header is present\n- Run [`cargo workspace publish --force omnix-cli`](https://github.com/pksunkara/cargo-workspaces?tab=readme-ov-file#publish) in devShell, using the picked version.\n"
  },
  {
    "path": "bacon.toml",
    "content": "# This is a configuration file for the bacon tool\n#\n# Bacon repository: https://github.com/Canop/bacon\n# Complete help on configuration: https://dystroy.org/bacon/config/\n# You can also check bacon's own bacon.toml file\n#  as an example: https://github.com/Canop/bacon/blob/main/bacon.toml\n\ndefault_job = \"check\"\n\n[jobs.check]\ncommand = [\"cargo\", \"check\", \"--color\", \"always\"]\nneed_stdout = false\n\n[jobs.check-all]\ncommand = [\"cargo\", \"check\", \"--all-targets\", \"--color\", \"always\"]\nneed_stdout = false\n\n# Run clippy on the default target\n[jobs.clippy]\ncommand = [\"cargo\", \"clippy\", \"--color\", \"always\"]\nneed_stdout = false\n\n# Run clippy on all targets\n# To disable some lints, you may change the job this way:\n#    [jobs.clippy-all]\n#    command = [\n#        \"cargo\", \"clippy\",\n#        \"--all-targets\",\n#        \"--color\", \"always\",\n#    \t \"--\",\n#    \t \"-A\", \"clippy::bool_to_int_with_if\",\n#    \t \"-A\", \"clippy::collapsible_if\",\n#    \t \"-A\", \"clippy::derive_partial_eq_without_eq\",\n#    ]\n# need_stdout = false\n[jobs.clippy-all]\ncommand = [\"cargo\", \"clippy\", \"--all-targets\", \"--color\", \"always\"]\nneed_stdout = false\n\n# This job lets you run\n# - all tests: bacon test\n# - a specific test: bacon test -- config::test_default_files\n# - the tests of a package: bacon test -- -- -p config\n[jobs.test]\ncommand = [\n  \"cargo\",\n  \"test\",\n  \"--color\",\n  \"always\",\n  \"--\",\n  \"--color\",\n  \"always\",  # see https://github.com/Canop/bacon/issues/124\n]\nneed_stdout = true\n\n[jobs.doc]\ncommand = [\"cargo\", \"doc\", \"--color\", \"always\", \"--no-deps\"]\nneed_stdout = false\n\n# If the doc compiles, then it opens in your browser and bacon switches\n# to the previous job\n[jobs.doc-open]\ncommand = [\"cargo\", \"doc\", \"--color\", \"always\", \"--no-deps\", \"--open\"]\nneed_stdout = false\non_success = \"back\"                                                    # so that we don't open the browser at each change\n\n# You can run your application and have the result displayed in bacon,\n# *if* it makes sense for this crate.\n# Don't forget the `--color always` part or the errors won't be\n# properly parsed.\n# If your program never stops (eg a server), you may set `background`\n# to false to have the cargo run output immediately displayed instead\n# of waiting for program's end.\n[jobs.run]\ncommand = [\n  \"cargo\",\n  \"run\",\n  \"--color\",\n  \"always\",\n  # put launch parameters for your program behind a `--` separator\n]\nneed_stdout = true\nallow_warnings = true\nbackground = true\n\n[jobs.health-failing]\ncommand = [\n  \"cargo\",\n  \"run\",\n  \"--color\",\n  \"always\",\n  \"--\",\n  \"health\",\n  \"./crates/omnix-health/failing\",\n]\nneed_stdout = true\nallow_warnings = true\n\n[jobs.develop]\ncommand = [\n  \"cargo\",\n  \"run\",\n  \"--color\",\n  \"always\",\n  \"--\",\n  \"develop\",\n  \".\",\n]\nneed_stdout = true\nallow_warnings = true\n\n\n# You may define here keybindings that would be specific to\n# a project, for example a shortcut to launch a specific job.\n# Shortcuts to internal functions (scrolling, toggling, etc.)\n# should go in your personal global prefs.toml file instead.\n[keybindings]\n# alt-m = \"job:my-job\"\nc = \"job:clippy-all\"     # comment this to have 'c' run clippy on only the default target\nh = \"job:health-failing\"\n"
  },
  {
    "path": "crates/nix_rs/CHANGELOG.md",
    "content": "# Changelog\n\n## Unreleased\n\n- **`flake::url`**:\n  - Remove `qualified_attr` module\n- **`eval::nix_eval`**\n  - Display evaluation progress\n  - Decrease logging verbosity\n- **`flake::schema`**\n  - Don't hardcode flake schema types\n- **`config`**\n  - Don't enable flakes during `NixConfig::get`\n- Support Nix 2.20\n- **`flake::url`**\n  - Add `without_attr`, `get_attr`\n  - Simplify the return type of `RootQualifiedAttr::eval_flake`\n  - Add `AsRef`, `Deref`, `From<&Path>` instances for `FlakeUrl`\n  - `Path` instances for `FlakeUrl` no longer use the `path:` prefix (to avoid store copying)\n  - **`attr`**:\n    - Add `FlakeAttr::new` and `FlakeAttr::none` constructors\n  - `qualified_attr` - vastly simplify module\n- `flake::functions`:\n  - Add new module\n- **`flake::command`**:\n  - Add module, for `nix run`, `nix build` and `nix develop`\n- **`store`**:\n  - Add module (upstreamed from nixci)\n  - Add `StoreURI`\n  - Avoid running `nix-store` multiple times.\n- **`copy`**:\n  - Takes `NixCopyOptions` now.\n- **`env`**:\n  - use `whoami` crate to find the current user instead of depending on environment variable `USER`\n  - `NixEnv::detect`'s logging uses DEBUG level now (formerly INFO)\n  - Add Nix installer to `NixEnv`\n- **`command`\n  - `run_with_args` is now `run_with`, and takes a function that mutates the `Command` at will.\n  - Add `trace_cmd_with`\n- **`version`**:\n  - Add `NixVersion::get`\n- **`system_list`**: New module\n- **version_spec**: New `NixVersion` spec module\n\n## 1.0.0\n\n- **DeterminateSystems/flake-schemas**\n  - Allow overriding the `nix` CLI command.\n  - Switch to flake schema given by <https://github.com/DeterminateSystems/flake-schemas>\n- **`flake::schema::FlakeSchema`**\n  - Add `nixos_configurations`\n- **`flake::url`**\n  - `Flake::from_nix` explicitly takes `NixConfig` as argument, rather than implicitly running nix to get it.\n  - Remove string convertion implementations; use `std::parse` instead, and handle errors explicitly.\n  - Split attr code to its own module, `flake::url::attr`\n  - Introduce `flake::url::qualified_attr` module\n- **`eval`**\n  - `nix_eval_attr_json`\n    - No longer takes `default_if_missing`; instead (always) returns `None` if attribute is missing.\n    - Rename to `nix_eval_maybe` (as there is no non-JSON variant)\n- **`env::NixEnv`**\n  - Clarify error message when `$USER` is not set\n- **``command`**\n  - Add `NixCmd::get()` to return flakes-enabled global command\n  - `NixCmd::default()` returns the bare command (no experimental features enabled)\n- ``config``\n  - Add `NixConfig::get()` to get the once-created static value of `NixConfig`\n- `info`\n  - Add `NixInfo::get()` to get the once-created static value of `NixInfo`\n  - Rename `NixInfo::from_nix()` to `NixInfo::new()`; the latter explicitly takes `NixConfig`\n\n## [0.5.0](https://github.com/juspay/nix-rs/compare/0.4.0...0.5.0) (2024-06-05)\n\n### Features\n\n- Improve `with_flakes` to transform existing `NixCmd`\n([f936e54](https://github.com/juspay/nix-rs/commit/f936e5401d1bc9b82084cf7b49402a5ee1a3b733))\n- Add support for clap deriving\n([f61bd2c](https://github.com/juspay/nix-rs/commit/f61bd2c740a23a10bbb89dfbd3b77fd4b2a49bac))\n- Add `NixCmd::extra_access_tokens`\n([a287ab2](https://github.com/juspay/nix-rs/commit/a287ab2ad2d21db6ac89e4ce94c55446a02af241))\n\n## [0.4.0](https://github.com/juspay/nix-rs/compare/0.3.3...0.4.0) (2024-06-03)\n\n### Features\n\n- add `NixCmd::run_with_args`\n([47f3170](https://github.com/juspay/nix-rs/commit/47f3170d57b72089eb977620217613571c52f456))\n- add `FlakeUrl::with_attr`\n([1ff343d](https://github.com/juspay/nix-rs/commit/1ff343d25f1a633c3caf2d6f723bbd1c9e352cbc))\n\n### [0.3.3](https://github.com/juspay/nix-rs/compare/0.3.2...0.3.3) (2024-04-17)\n\n#### Features\n\n- **eval:** nix_eval_attr_json explicitly takes NixCmd\n([cccdb43](https://github.com/juspay/nix-rs/commit/cccdb437f4f2b31d32778e9cf3de2ab1a61d9331))\n- **command:** Add `with_flakes` returning smarter nix CLI with flakes enabled\n([f7f217a](https://github.com/juspay/nix-rs/commit/f7f217a12acefc3992b5ff8ba59d861f5cc2abcb))\n\n### 0.3.2 (2024-04-04)\n"
  },
  {
    "path": "crates/nix_rs/Cargo.toml",
    "content": "[package]\nname = \"nix_rs\"\n# Important: remember to update the top-level Cargo.toml if updating major version\nversion = \"1.3.2\"\nlicense = \"Apache-2.0\"\nrepository = \"https://github.com/juspay/omnix\"\ndescription = \"Rust library for interacting with the Nix command\"\nedition = \"2021\"\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\ncfg-if = { workspace = true }\nregex = { workspace = true }\nos_info = { workspace = true }\nthiserror = { workspace = true }\nserde = { workspace = true }\nserde_qs = { workspace = true }\nserde_json = { workspace = true }\nserde_with = { workspace = true }\ntokio = { workspace = true }\ntracing = { workspace = true }\nurl = { workspace = true }\ncolored = { workspace = true }\nshell-words = { workspace = true }\nis_proc_translated = { workspace = true }\nsysinfo = { workspace = true }\ntempfile = { workspace = true }\nbytesize = { workspace = true }\nclap = { workspace = true, optional = true }\nnonempty = { workspace = true }\nwhoami = { workspace = true }\nlazy_static = { workspace = true }\nwhich = { workspace = true }\n\n[features]\nclap = [\"dep:clap\"]\n"
  },
  {
    "path": "crates/nix_rs/README.md",
    "content": "# nix_rs\n\n[![Crates.io](https://img.shields.io/crates/v/nix_rs.svg)](https://crates.io/crates/nix_rs)\n\nA Rust crate to interact with the [Nix](https://nixos.asia/en/nix) command. `nix_rs` also provides the needed Rust types which are guaranteed to compile in wasm.\n"
  },
  {
    "path": "crates/nix_rs/crate.nix",
    "content": "{\n  autoWire = [ ];\n  crane = {\n    args = {\n      nativeBuildInputs = [\n        # nix # Tests need nix cli\n      ];\n    };\n  };\n}\n"
  },
  {
    "path": "crates/nix_rs/src/arg.rs",
    "content": "//! Nix command's arguments\n\nuse std::collections::HashMap;\n\nuse serde::{Deserialize, Serialize};\n\n/// All arguments you can pass to the `nix` command\n///\n/// This struct is clap-friendly for using in your subcommands. The clap options will mirror that of `nix`.\n///\n/// To convert to `Command` args list, use `into_iter`.\n#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]\n#[cfg_attr(feature = \"clap\", derive(clap::Parser))]\npub struct NixArgs {\n    /// Append to the experimental-features setting of Nix.\n    #[cfg_attr(feature = \"clap\", arg(long))]\n    pub extra_experimental_features: Vec<String>,\n\n    /// Append to the access-tokens setting of Nix.\n    #[cfg_attr(feature = \"clap\", arg(long))]\n    pub extra_access_tokens: Vec<String>,\n\n    /// Additional arguments to pass through to `nix`\n    ///\n    /// NOTE: Arguments irrelevant to a nix subcommand will automatically be ignored.\n    #[cfg_attr(feature = \"clap\", arg(last = true))]\n    pub extra_nix_args: Vec<String>,\n}\n\nimpl NixArgs {\n    /// Convert this [NixCmd] configuration into a list of arguments for\n    /// [Command]\n    pub fn to_args(&self, subcommands: &[&str]) -> Vec<String> {\n        let mut args = vec![];\n        if !self.extra_experimental_features.is_empty() {\n            args.push(\"--extra-experimental-features\".to_string());\n            args.push(self.extra_experimental_features.join(\" \"));\n        }\n        if !self.extra_access_tokens.is_empty() {\n            args.push(\"--extra-access-tokens\".to_string());\n            args.push(self.extra_access_tokens.join(\" \"));\n        }\n        let mut extra_nix_args = self.extra_nix_args.clone();\n        remove_nonsense_args_when_subcommand(subcommands, &mut extra_nix_args);\n        args.extend(extra_nix_args);\n        args\n    }\n\n    /// Enable flakes on this [NixCmd] configuration\n    pub fn with_flakes(&mut self) {\n        self.extra_experimental_features\n            .append(vec![\"nix-command\".to_string(), \"flakes\".to_string()].as_mut());\n    }\n\n    /// Enable nix-command on this [NixCmd] configuration\n    pub fn with_nix_command(&mut self) {\n        self.extra_experimental_features\n            .append(vec![\"nix-command\".to_string()].as_mut());\n    }\n}\n\n/// Certain options, like --rebuild, is not supported by all subcommands (e.g.\n/// `nix develop`). We remove them here. Yes, this is a bit of HACK!\nfn remove_nonsense_args_when_subcommand(subcommands: &[&str], args: &mut Vec<String>) {\n    let unsupported = non_sense_options(subcommands);\n    for (option, count) in unsupported {\n        remove_arguments(args, option, count);\n    }\n}\n\nfn non_sense_options<'a>(subcommands: &[&str]) -> HashMap<&'a str, usize> {\n    let rebuild = (\"--rebuild\", 0);\n    let override_input = (\"--override-input\", 2);\n    match subcommands {\n        [\"eval\"] => HashMap::from([rebuild, override_input]),\n        [\"flake\", \"lock\"] => HashMap::from([rebuild, override_input]),\n        [\"flake\", \"check\"] => HashMap::from([rebuild]),\n        [\"develop\"] => HashMap::from([rebuild]),\n        [\"run\"] => HashMap::from([rebuild]),\n        _ => HashMap::new(),\n    }\n}\n\nfn remove_arguments(vec: &mut Vec<String>, arg: &str, next: usize) {\n    let mut i = 0;\n    while i < vec.len() {\n        if vec[i] == arg && i + next < vec.len() {\n            vec.drain(i..i + next + 1);\n        } else {\n            i += 1;\n        }\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/command.rs",
    "content": "//! Nix base command configuration\n//!\n//! # Example\n//!\n//! ```ignore\n//! use nix_rs::command::NixCmd;\n//! let cmd = NixCmd::default();\n//! cmd.run_with_args_returning_stdout(&[\"--version\"]);\n//! ```\n\nuse std::{\n    fmt::{self, Display},\n    process::Stdio,\n};\n\nuse serde::{Deserialize, Serialize};\nuse thiserror::Error;\n\nuse tokio::{process::Command, sync::OnceCell};\n\nuse tracing::instrument;\n\n#[cfg(feature = \"clap\")]\nuse clap;\n\nuse crate::{arg::NixArgs, config::NixConfig};\n\n/// The `nix` command's global options.\n///\n/// See [available global\n/// options](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix#options)\n#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]\n#[cfg_attr(feature = \"clap\", derive(clap::Parser))]\npub struct NixCmd {\n    /// The arguments to pass to `nix`\n    #[cfg_attr(feature = \"clap\", clap(flatten))]\n    pub args: NixArgs,\n}\n\nstatic NIXCMD: OnceCell<NixCmd> = OnceCell::const_new();\n\n/// Trace a user-copyable command line\n///\n/// [tracing::info!] the given [tokio::process::Command] with human-readable\n/// command-line string that can generally be copy-pasted by the user.\n///\n/// The command will be highlighted to distinguish it (for copying) from the\n/// rest of the instrumentation parameters.\n#[instrument(name = \"command\")]\npub fn trace_cmd(cmd: &tokio::process::Command) {\n    trace_cmd_with(\"❄️ \", cmd);\n}\n\n/// Like [trace_cmd] but with a custom icon\n#[instrument(name = \"command\")]\npub fn trace_cmd_with(icon: &str, cmd: &tokio::process::Command) {\n    use colored::Colorize;\n    tracing::info!(\"{}\", format!(\"{} {}️\", icon, to_cli(cmd)).dimmed());\n}\n\nimpl NixCmd {\n    /// Return a global `NixCmd` instance with flakes enabled.\n    pub async fn get() -> &'static NixCmd {\n        NIXCMD\n            .get_or_init(|| async {\n                let cfg = NixConfig::get().await.as_ref().unwrap_or_else(|err| {\n                    panic!(\"Unable to get Nix config. Is your nix.conf valid?\\n{}\", err)\n                });\n                let mut cmd = NixCmd::default();\n                if !cfg.is_flakes_enabled() {\n                    cmd.args.with_flakes()\n                }\n                cmd\n            })\n            .await\n    }\n\n    /// Return a [Command] for this [NixCmd] configuration\n    ///\n    /// Arguments:\n    /// - `subcommands`: Optional subcommands to pass. Note that `NixArgs` will\n    ///   be passed *after* these subcommands.\n    pub fn command(&self, subcommands: &[&str]) -> Command {\n        let mut cmd = Command::new(\"nix\");\n        cmd.kill_on_drop(true);\n        cmd.args(subcommands);\n        cmd.args(self.args.to_args(subcommands));\n        cmd\n    }\n\n    /// Run nix with given args, interpreting stdout as JSON, parsing into `T`\n    pub async fn run_with_args_expecting_json<T>(\n        &self,\n        subcommands: &[&str],\n        args: &[&str],\n    ) -> Result<T, NixCmdError>\n    where\n        T: serde::de::DeserializeOwned,\n    {\n        let stdout: Vec<u8> = self\n            .run_with_returning_stdout(subcommands, |c| {\n                c.args(args);\n            })\n            .await?;\n        let v = serde_json::from_slice::<T>(&stdout)?;\n        Ok(v)\n    }\n\n    /// Run nix with given args, interpreting parsing stdout, via [std::str::FromStr], into `T`\n    pub async fn run_with_args_expecting_fromstr<T>(\n        &self,\n        subcommands: &[&str],\n        args: &[&str],\n    ) -> Result<T, NixCmdError>\n    where\n        T: std::str::FromStr,\n        <T as std::str::FromStr>::Err: std::fmt::Display,\n    {\n        let stdout = self\n            .run_with_returning_stdout(subcommands, |c| {\n                c.args(args);\n            })\n            .await?;\n        let v = &String::from_utf8_lossy(&stdout);\n        let v = T::from_str(v.trim()).map_err(|e| FromStrError(e.to_string()))?;\n        Ok(v)\n    }\n\n    /// Like [Self::run_with] but returns stdout as a [`Vec<u8>`]\n    pub async fn run_with_returning_stdout<F>(\n        &self,\n        subcommands: &[&str],\n        f: F,\n    ) -> Result<Vec<u8>, CommandError>\n    where\n        F: FnOnce(&mut Command),\n    {\n        let mut cmd = self.command(subcommands);\n        f(&mut cmd);\n        trace_cmd(&cmd);\n\n        cmd.stdout(Stdio::piped());\n        cmd.stderr(Stdio::piped());\n        let child = cmd.spawn()?;\n        let out = child.wait_with_output().await?;\n\n        if out.status.success() {\n            Ok(out.stdout)\n        } else {\n            let stderr = String::from_utf8_lossy(&out.stderr).to_string();\n            Err(CommandError::ProcessFailed {\n                stderr,\n                exit_code: out.status.code(),\n            })\n        }\n    }\n\n    /// Run Nix with given [Command] customizations, while also tracing the command being run.\n    ///\n    /// Return the stdout bytes returned by [tokio::process::Child::wait_with_output]. In order to capture stdout, you must call `cmd.stdout(Stdio::piped());` inside the handler.\n    pub async fn run_with<F>(&self, subcommands: &[&str], f: F) -> Result<Vec<u8>, CommandError>\n    where\n        F: FnOnce(&mut Command),\n    {\n        let mut cmd = self.command(subcommands);\n        f(&mut cmd);\n        trace_cmd(&cmd);\n        let out = cmd.spawn()?.wait_with_output().await?;\n        if out.status.success() {\n            Ok(out.stdout)\n        } else {\n            let stderr = String::from_utf8_lossy(&out.stderr).to_string();\n            Err(CommandError::ProcessFailed {\n                stderr,\n                exit_code: out.status.code(),\n            })\n        }\n    }\n}\n\n/// Convert a Command to user-copyable CLI string\nfn to_cli(cmd: &tokio::process::Command) -> String {\n    use std::ffi::OsStr;\n    let program = cmd.as_std().get_program().to_string_lossy().to_string();\n    let args = cmd\n        .as_std()\n        .get_args()\n        .collect::<Vec<&OsStr>>()\n        .into_iter()\n        .map(|s| s.to_string_lossy().to_string())\n        .collect::<Vec<String>>();\n    let cli = vec![program]\n        .into_iter()\n        .chain(args)\n        .collect::<Vec<String>>();\n    shell_words::join(cli)\n}\n\n/// Errors when running and interpreting the output of a nix command\n#[derive(Error, Debug)]\npub enum NixCmdError {\n    /// A [CommandError]\n    #[error(\"Command error: {0}\")]\n    CmdError(#[from] CommandError),\n\n    /// Failed to unicode-decode the output of a command\n    #[error(\"Failed to decode command stdout (utf8 error): {0}\")]\n    DecodeErrorUtf8(#[from] std::string::FromUtf8Error),\n\n    /// Failed to parse the output of a command\n    #[error(\"Failed to decode command stdout (from_str error): {0}\")]\n    DecodeErrorFromStr(#[from] FromStrError),\n\n    /// Failed to parse the output of a command as JSON\n    #[error(\"Failed to decode command stdout (json error): {0}\")]\n    DecodeErrorJson(#[from] serde_json::Error),\n}\n\n/// Errors when parsing a string into a type\n#[derive(Debug)]\npub struct FromStrError(String);\n\nimpl Display for FromStrError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"Failed to parse string: {}\", self.0)\n    }\n}\n\nimpl std::error::Error for FromStrError {}\n\n/// Errors when running a command\n#[derive(Error, Debug)]\npub enum CommandError {\n    /// Error when spawning a child process\n    #[error(\"Child process error: {0}\")]\n    ChildProcessError(#[from] std::io::Error),\n\n    /// Child process exited unsuccessfully\n    #[error(\n        \"Process exited unsuccessfully. exit_code={:?} stderr={}\",\n        exit_code,\n        stderr\n    )]\n    ProcessFailed {\n        /// The stderr of the process, if available.\n        stderr: String,\n        /// The exit code of the process\n        exit_code: Option<i32>,\n    },\n\n    /// Failed to decode the stderr of a command\n    #[error(\"Failed to decode command stderr: {0}\")]\n    Decode(#[from] std::string::FromUtf8Error),\n}\n"
  },
  {
    "path": "crates/nix_rs/src/config.rs",
    "content": "//! Rust module for `nix show-config`\n\nuse std::{convert::Infallible, str::FromStr};\n\nuse serde::{Deserialize, Serialize};\nuse serde_with::DeserializeFromStr;\nuse tokio::sync::OnceCell;\nuse tracing::instrument;\nuse url::Url;\n\nuse crate::{\n    command::{NixCmd, NixCmdError},\n    version::NixVersion,\n};\n\nuse super::flake::system::System;\n\n/// Nix configuration spit out by `nix show-config`\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\npub struct NixConfig {\n    /// Number of CPU cores used for nix builds\n    pub cores: ConfigVal<i32>,\n    /// Experimental features currently enabled\n    pub experimental_features: ConfigVal<Vec<String>>,\n    /// Extra platforms to build for\n    pub extra_platforms: ConfigVal<Vec<String>>,\n    /// The flake registry to use to lookup atomic flake inputs\n    pub flake_registry: ConfigVal<String>,\n    /// Maximum number of jobs to run in parallel\n    pub max_jobs: ConfigVal<i32>,\n    /// Cache substituters\n    pub substituters: ConfigVal<Vec<Url>>,\n    /// Current system\n    pub system: ConfigVal<System>,\n    /// Trusted users\n    pub trusted_users: ConfigVal<Vec<TrustedUserValue>>,\n}\n\n/// The value for each 'nix show-config --json' key.\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct ConfigVal<T> {\n    /// Current value in use.\n    pub value: T,\n    /// Default value by Nix.\n    pub default_value: T,\n    /// Description of this config item.\n    pub description: String,\n}\n\nstatic NIX_CONFIG: OnceCell<Result<NixConfig, NixConfigError>> = OnceCell::const_new();\n\nstatic NIX_2_20_0: NixVersion = NixVersion {\n    major: 2,\n    minor: 20,\n    patch: 0,\n};\n\nimpl NixConfig {\n    /// Get the once version of `NixConfig`.\n    #[instrument(name = \"show-config(once)\")]\n    pub async fn get() -> &'static Result<NixConfig, NixConfigError> {\n        NIX_CONFIG\n            .get_or_init(|| async {\n                let mut cmd = NixCmd::default();\n                cmd.args.with_nix_command(); // Enable nix-command, since don't yet know if it is already enabled.\n                let nix_ver = NixVersion::get().await.as_ref()?;\n                let cfg = NixConfig::from_nix(&cmd, nix_ver).await?;\n                Ok(cfg)\n            })\n            .await\n    }\n\n    /// Get the output of `nix show-config`\n    #[instrument(name = \"show-config\")]\n    pub async fn from_nix(\n        nix_cmd: &super::command::NixCmd,\n        nix_version: &NixVersion,\n    ) -> Result<NixConfig, super::command::NixCmdError> {\n        let v = if nix_version >= &NIX_2_20_0 {\n            nix_cmd\n                .run_with_args_expecting_json(&[\"config\", \"show\"], &[\"--json\"])\n                .await?\n        } else {\n            nix_cmd\n                .run_with_args_expecting_json(&[\"show-config\"], &[\"--json\"])\n                .await?\n        };\n        Ok(v)\n    }\n\n    /// Is flakes and command features enabled?\n    pub fn is_flakes_enabled(&self) -> bool {\n        self.experimental_features\n            .value\n            .contains(&\"nix-command\".to_string())\n            && self\n                .experimental_features\n                .value\n                .contains(&\"flakes\".to_string())\n    }\n}\n\n/// Error type for `NixConfig`\n#[derive(thiserror::Error, Debug)]\npub enum NixConfigError {\n    /// A [NixCmdError]\n    #[error(\"Nix command error: {0}\")]\n    NixCmdError(#[from] NixCmdError),\n\n    /// A [NixCmdError] with a static lifetime\n    #[error(\"Nix command error: {0}\")]\n    NixCmdErrorStatic(#[from] &'static NixCmdError),\n}\n\n/// Accepted value for \"trusted-users\" in nix.conf\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, DeserializeFromStr)]\npub enum TrustedUserValue {\n    /// All users are trusted\n    All,\n    /// A specific user is trusted\n    User(String),\n    /// Users belonging to a specific group are trusted\n    Group(String),\n}\n\nimpl TrustedUserValue {\n    fn from_str(s: &str) -> Self {\n        // In nix.conf, groups are prefixed with '@'. '*' means all users are\n        // trusted.\n        if s == \"*\" {\n            return Self::All;\n        }\n        match s.strip_prefix('@') {\n            Some(s) => Self::Group(s.to_string()),\n            None => Self::User(s.to_string()),\n        }\n    }\n\n    /// Display the nix.conf original string\n    pub fn display_original(val: &[TrustedUserValue]) -> String {\n        val.iter()\n            .map(|x| match x {\n                TrustedUserValue::All => \"*\".to_string(),\n                TrustedUserValue::User(x) => x.to_string(),\n                TrustedUserValue::Group(x) => format!(\"@{}\", x),\n            })\n            .collect::<Vec<String>>()\n            .join(\" \")\n    }\n}\n\nimpl From<String> for TrustedUserValue {\n    fn from(s: String) -> Self {\n        Self::from_str(&s)\n    }\n}\n\nimpl FromStr for TrustedUserValue {\n    type Err = Infallible;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        Ok(Self::from_str(s))\n    }\n}\n\n#[tokio::test]\nasync fn test_nix_config() -> Result<(), crate::command::NixCmdError> {\n    let v = NixConfig::get().await.as_ref().unwrap();\n    println!(\"Max Jobs: {}\", v.max_jobs.value);\n    Ok(())\n}\n"
  },
  {
    "path": "crates/nix_rs/src/copy.rs",
    "content": "//! Rust module for `nix copy`.\nuse crate::{\n    command::{CommandError, NixCmd},\n    store::uri::StoreURI,\n};\nuse std::{ffi::OsStr, path::Path};\n\n/// Options for `nix copy`.\n#[derive(Debug, Clone, Default)]\npub struct NixCopyOptions {\n    /// The URI of the store to copy from.\n    pub from: Option<StoreURI>,\n    /// The URI of the store to copy to.\n    pub to: Option<StoreURI>,\n    /// Do not check signatures.\n    pub no_check_sigs: bool,\n}\n\n/// Copy store paths to a remote Nix store using `nix copy`.\n///\n/// # Arguments\n///\n/// * `cmd` - The `nix` command\n/// * `host` - The remote host to copy to\n/// * `paths` - The paths to copy. Limit this to be within the limit of Unix process arguments size limit.\npub async fn nix_copy<I, P>(\n    cmd: &NixCmd,\n    options: NixCopyOptions,\n    paths: I,\n) -> Result<(), CommandError>\nwhere\n    I: IntoIterator<Item = P>,\n    P: AsRef<Path> + AsRef<OsStr>,\n{\n    cmd.run_with(&[\"copy\"], |cmd| {\n        cmd.arg(\"-v\");\n        if let Some(uri) = options.from {\n            cmd.arg(\"--from\").arg(uri.to_string());\n        }\n        if let Some(uri) = options.to {\n            cmd.arg(\"--to\").arg(uri.to_string());\n        }\n        if options.no_check_sigs {\n            cmd.arg(\"--no-check-sigs\");\n        }\n        cmd.args(paths);\n    })\n    .await?;\n    Ok(())\n}\n"
  },
  {
    "path": "crates/nix_rs/src/detsys_installer.rs",
    "content": "//! DetSys installer detection\n// TODO: Move this under 'env' module.\nuse serde::{Deserialize, Serialize};\n\nuse std::{fmt::Display, io::ErrorKind, path::Path, str::FromStr};\n\nuse regex::Regex;\nuse thiserror::Error;\n\n/// The installer from <https://github.com/DeterminateSystems/nix-installer>\n#[derive(Debug, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Clone)]\npub struct DetSysNixInstaller {\n    version: InstallerVersion,\n}\n\nimpl DetSysNixInstaller {\n    /// Detects if the DetSys nix-installer is installed\n    pub fn detect() -> Result<Option<Self>, BadInstallerVersion> {\n        let nix_installer_path = Path::new(\"/nix/nix-installer\");\n        if nix_installer_path.exists() {\n            Ok(Some(DetSysNixInstaller {\n                version: InstallerVersion::get_version(nix_installer_path)?,\n            }))\n        } else {\n            Ok(None)\n        }\n    }\n}\n\nimpl Display for DetSysNixInstaller {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"DetSys nix-installer ({})\", self.version)\n    }\n}\n\n// The version of Detsys/nix-installer\n#[derive(Debug, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Clone)]\nstruct InstallerVersion {\n    major: u32,\n    minor: u32,\n    patch: u32,\n}\n\nimpl Display for InstallerVersion {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}.{}.{}\", self.major, self.minor, self.patch)\n    }\n}\n\n/// Errors that can occur when trying to get the [DetSysNixInstaller] version\n#[derive(Error, Debug)]\npub enum BadInstallerVersion {\n    /// Regex error\n    #[error(\"Regex error: {0}\")]\n    Regex(#[from] regex::Error),\n\n    /// Failed to decode installer output\n    #[error(\"Failed to decode installer output: {0}\")]\n    Decode(#[from] std::string::FromUtf8Error),\n\n    /// Failed to parse installer version\n    #[error(\"Failed to parse installer version: {0}\")]\n    Parse(#[from] std::num::ParseIntError),\n\n    /// Failed to fetch installer version\n    #[error(\"Failed to fetch installer version: {0}\")]\n    Command(std::io::Error),\n}\n\nimpl FromStr for InstallerVersion {\n    type Err = BadInstallerVersion;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        let re = Regex::new(r\"(\\d+)\\.(\\d+)\\.(\\d+)\")?;\n\n        let captures = re\n            .captures(s)\n            .ok_or(BadInstallerVersion::Command(std::io::Error::new(\n                ErrorKind::InvalidData,\n                \"Failed to capture regex\",\n            )))?;\n        let major = captures[1].parse::<u32>()?;\n        let minor = captures[2].parse::<u32>()?;\n        let patch = captures[3].parse::<u32>()?;\n\n        Ok(InstallerVersion {\n            major,\n            minor,\n            patch,\n        })\n    }\n}\n\nimpl InstallerVersion {\n    pub fn get_version(executable_path: &Path) -> Result<Self, BadInstallerVersion> {\n        let output = std::process::Command::new(executable_path)\n            .arg(\"--version\")\n            .output()\n            .map_err(BadInstallerVersion::Command)?;\n        let version_str = String::from_utf8(output.stdout)?;\n        version_str.parse()\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/env.rs",
    "content": "//! Information about the environment in which Nix will run\n// TODO: Make this a package, and split (alongn with detsys_installer.rs)\nuse std::{fmt::Display, path::Path};\n\nuse bytesize::ByteSize;\nuse os_info;\nuse serde::{Deserialize, Serialize};\nuse serde_with::SerializeDisplay;\nuse std::process::Command;\nuse tracing::instrument;\nuse whoami;\n\n/// The environment in which Nix operates\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct NixEnv {\n    /// Current user ($USER)\n    pub current_user: String,\n    /// Current user groups\n    pub current_user_groups: Vec<String>,\n    /// Underlying OS in which Nix runs\n    pub os: OS,\n    /// Total disk space of the volume where /nix exists.\n    ///\n    /// This is either root volume or the dedicated /nix volume.\n    pub total_disk_space: ByteSize,\n    /// Total memory\n    pub total_memory: ByteSize,\n    /// The installer used to install Nix\n    pub installer: NixInstaller,\n}\n\nimpl NixEnv {\n    /// Determine [NixEnv] on the user's system\n\n    #[instrument]\n    pub async fn detect() -> Result<NixEnv, NixEnvError> {\n        use sysinfo::{DiskExt, SystemExt};\n        tracing::debug!(\"Detecting Nix environment\");\n        let os = OS::detect().await;\n        tokio::task::spawn_blocking(|| {\n            let current_user = whoami::username();\n            let sys = sysinfo::System::new_with_specifics(\n                sysinfo::RefreshKind::new().with_disks_list().with_memory(),\n            );\n            let total_disk_space = to_bytesize(get_nix_disk(&sys)?.total_space());\n            let total_memory = to_bytesize(sys.total_memory());\n            let current_user_groups = get_current_user_groups()?;\n            let installer = NixInstaller::detect()?;\n            Ok(NixEnv {\n                current_user,\n                current_user_groups,\n                os,\n                total_disk_space,\n                total_memory,\n                installer,\n            })\n        })\n        .await\n        .unwrap()\n    }\n}\n\n/// Get the current user's groups\nfn get_current_user_groups() -> Result<Vec<String>, NixEnvError> {\n    let output = Command::new(\"groups\")\n        .output()\n        .map_err(NixEnvError::GroupsError)?;\n    let group_info = &String::from_utf8_lossy(&output.stdout);\n    Ok(group_info\n        .as_ref()\n        .split_whitespace()\n        .map(|v| v.to_string())\n        .collect())\n}\n\n/// Get the disk where /nix exists\nfn get_nix_disk(sys: &sysinfo::System) -> Result<&sysinfo::Disk, NixEnvError> {\n    use sysinfo::{DiskExt, SystemExt};\n    let by_mount_point: std::collections::HashMap<&Path, &sysinfo::Disk> = sys\n        .disks()\n        .iter()\n        .map(|disk| (disk.mount_point(), disk))\n        .collect();\n    // Lookup /nix first, then /.\n    by_mount_point\n        .get(Path::new(\"/nix\"))\n        .copied()\n        .or_else(|| by_mount_point.get(Path::new(\"/\")).copied())\n        .ok_or(NixEnvError::NoDisk)\n}\n\n/// The system under which Nix is installed and operates\n#[derive(Debug, Clone, PartialEq, Eq, SerializeDisplay, Deserialize)]\npub enum OS {\n    /// On macOS\n    MacOS {\n        /// Using nix-darwin\n        nix_darwin: bool,\n        /// Architecture\n        arch: Option<String>,\n        /// https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment\n        proc_translated: bool,\n    },\n    /// On NixOS\n    NixOS,\n    /// Nix is individually installed on Linux or macOS\n    Other(os_info::Type),\n}\n\n// The [Display] instance affects how [OS] is displayed to the app user\nimpl Display for OS {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            OS::MacOS {\n                nix_darwin,\n                arch: _,\n                proc_translated: _,\n            } => {\n                if *nix_darwin {\n                    write!(f, \"macOS (nix-darwin)\")\n                } else {\n                    write!(f, \"macOS\")\n                }\n            }\n            OS::NixOS => write!(f, \"NixOS\"),\n            OS::Other(os_type) => write!(f, \"{}\", os_type),\n        }\n    }\n}\n\nimpl OS {\n    /// Detect the OS\n    pub async fn detect() -> Self {\n        let os_info = tokio::task::spawn_blocking(os_info::get).await.unwrap();\n        let os_type = os_info.os_type();\n        let arch = os_info.architecture();\n        async fn is_symlink(file_path: &str) -> std::io::Result<bool> {\n            let metadata = tokio::fs::symlink_metadata(file_path).await?;\n            Ok(metadata.file_type().is_symlink())\n        }\n        match os_type {\n            os_info::Type::Macos => {\n                // To detect that we are on NixDarwin, we check if /etc/nix/nix.conf\n                // is a symlink (which nix-darwin manages like NixOS does)\n                let nix_darwin = is_symlink(\"/etc/nix/nix.conf\").await.unwrap_or(false);\n                OS::MacOS {\n                    nix_darwin,\n                    arch: arch.map(|s| s.to_string()),\n                    proc_translated: is_proc_translated::is_proc_translated(),\n                }\n            }\n            os_info::Type::NixOS => OS::NixOS,\n            _ => OS::Other(os_type),\n        }\n    }\n\n    /// Return the label for nix-darwin or NixOS system\n    pub fn nix_system_config_label(&self) -> Option<String> {\n        // TODO: This should return Markdown\n        match self {\n            OS::MacOS {\n                nix_darwin,\n                arch: _,\n                proc_translated: _,\n            } if *nix_darwin => Some(\"nix-darwin configuration\".to_string()),\n            OS::NixOS => Some(\"nixos configuration\".to_string()),\n            _ => None,\n        }\n    }\n\n    /// Return the label for where Nix is configured\n    pub fn nix_config_label(&self) -> String {\n        self.nix_system_config_label()\n            .unwrap_or(\"/etc/nix/nix.conf\".to_string())\n    }\n}\n\n/// The installer used to install Nix (applicable only for non-NixOS systems)\n#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]\n#[serde(tag = \"type\")]\npub enum NixInstaller {\n    /// The Determinate Systems installer\n    DetSys(super::detsys_installer::DetSysNixInstaller),\n    /// Either offical installer or from a different package manager\n    Other,\n}\n\nimpl Display for NixInstaller {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            NixInstaller::DetSys(installer) => write!(f, \"{}\", installer),\n            NixInstaller::Other => {\n                write!(f, \"Unknown installer\")\n            }\n        }\n    }\n}\n\nimpl NixInstaller {\n    /// Detect the Nix installer\n    pub fn detect() -> Result<Self, NixEnvError> {\n        match super::detsys_installer::DetSysNixInstaller::detect()? {\n            Some(installer) => Ok(NixInstaller::DetSys(installer)),\n            None => Ok(NixInstaller::Other),\n        }\n    }\n}\n\n/// Errors while trying to fetch [NixEnv]\n#[derive(thiserror::Error, Debug)]\npub enum NixEnvError {\n    /// Unable to find user groups\n    #[error(\"Failed to fetch groups: {0}\")]\n    GroupsError(std::io::Error),\n\n    /// Unable to find /nix volume\n    #[error(\"Unable to find root disk or /nix volume\")]\n    NoDisk,\n\n    /// Unable to find Nix installer\n    #[error(\"Failed to detect Nix installer: {0}\")]\n    InstallerError(#[from] super::detsys_installer::BadInstallerVersion),\n\n    /// `nix` command not found\n    #[error(\"`nix` not found in PATH: {0}\")]\n    NixPathError(#[from] which::Error),\n}\n\n/// Convert bytes to a closest [ByteSize]\n///\n/// Useful for displaying disk space and memory which are typically in GBs / TBs\nfn to_bytesize(bytes: u64) -> ByteSize {\n    let kb = bytes / 1024;\n    let mb = kb / 1024;\n    let gb = mb / 1024;\n    if gb > 0 {\n        ByteSize::gib(gb)\n    } else if mb > 0 {\n        ByteSize::mib(mb)\n    } else if kb > 0 {\n        ByteSize::kib(kb)\n    } else {\n        ByteSize::b(bytes)\n    }\n}\n\n/// Test for [to_bytesize]\n\n#[test]\nfn test_to_bytesize() {\n    assert_eq!(to_bytesize(0), ByteSize::b(0));\n    assert_eq!(to_bytesize(1), ByteSize::b(1));\n    assert_eq!(to_bytesize(1023), ByteSize::b(1023));\n    assert_eq!(to_bytesize(1024), ByteSize::kib(1));\n    assert_eq!(to_bytesize(1024 * 1024), ByteSize::mib(1));\n    assert_eq!(to_bytesize(1024 * 1024 * 1024), ByteSize::gib(1));\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/command.rs",
    "content": "//! Nix commands for working with flakes\nuse std::{\n    collections::{BTreeMap, HashMap},\n    path::PathBuf,\n};\n\nuse nonempty::NonEmpty;\nuse serde::{Deserialize, Serialize};\nuse tokio::process::Command;\n\nuse crate::command::{CommandError, NixCmd, NixCmdError};\n\nuse super::url::FlakeUrl;\n\n/// Run `nix run` on the given flake app.\npub async fn run(\n    nixcmd: &NixCmd,\n    opts: &FlakeOptions,\n    url: &FlakeUrl,\n    args: Vec<String>,\n) -> Result<(), CommandError> {\n    nixcmd\n        .run_with(&[\"run\"], |cmd| {\n            opts.use_in_command(cmd);\n            cmd.args([url.to_string(), \"--\".to_string()]);\n            cmd.args(args);\n        })\n        .await?;\n    Ok(())\n}\n\n/// Run `nix develop` on the given flake devshell.\npub async fn develop(\n    nixcmd: &NixCmd,\n    opts: &FlakeOptions,\n    url: &FlakeUrl,\n    command: NonEmpty<String>,\n) -> Result<(), CommandError> {\n    nixcmd\n        .run_with(&[\"develop\"], |cmd| {\n            opts.use_in_command(cmd);\n            cmd.args([url.to_string(), \"-c\".to_string()]);\n            cmd.args(command);\n        })\n        .await?;\n    Ok(())\n}\n\n/// Run `nix build`\npub async fn build(\n    cmd: &NixCmd,\n    opts: &FlakeOptions,\n    url: FlakeUrl,\n) -> Result<Vec<OutPath>, NixCmdError> {\n    let stdout: Vec<u8> = cmd\n        .run_with_returning_stdout(&[\"build\"], |c| {\n            opts.use_in_command(c);\n            c.args([\"--no-link\", \"--json\", &url]);\n        })\n        .await?;\n    let v = serde_json::from_slice::<Vec<OutPath>>(&stdout)?;\n    Ok(v)\n}\n\n/// Run `nix flake lock`\npub async fn lock(\n    cmd: &NixCmd,\n    opts: &FlakeOptions,\n    args: &[&str],\n    url: &FlakeUrl,\n) -> Result<(), NixCmdError> {\n    cmd.run_with(&[\"flake\", \"lock\"], |c| {\n        c.arg(url.to_string());\n        opts.use_in_command(c);\n        c.args(args);\n    })\n    .await?;\n    Ok(())\n}\n\n/// Run `nix flake check`\npub async fn check(cmd: &NixCmd, opts: &FlakeOptions, url: &FlakeUrl) -> Result<(), NixCmdError> {\n    cmd.run_with(&[\"flake\", \"check\"], |c| {\n        c.arg(url.to_string());\n        opts.use_in_command(c);\n    })\n    .await?;\n    Ok(())\n}\n\n/// A path built by nix, as returned by --print-out-paths\n#[derive(Serialize, Deserialize)]\npub struct OutPath {\n    /// The derivation that built these outputs\n    #[serde(rename = \"drvPath\")]\n    pub drv_path: PathBuf,\n    /// Build outputs\n    pub outputs: HashMap<String, PathBuf>,\n}\n\nimpl OutPath {\n    /// Return the first build output, if any\n    pub fn first_output(&self) -> Option<&PathBuf> {\n        self.outputs.values().next()\n    }\n}\n\n/// Nix CLI options when interacting with a flake\n#[derive(Debug, Clone, Default)]\npub struct FlakeOptions {\n    /// The --override-input option to pass to Nix\n    pub override_inputs: BTreeMap<String, FlakeUrl>,\n\n    /// Pass --no-write-lock-file\n    pub no_write_lock_file: bool,\n\n    /// The directory from which to run our nix command (such that relative flake URLs resolve properly)\n    pub current_dir: Option<PathBuf>,\n}\n\nimpl FlakeOptions {\n    /// Apply these options to a (Nix) [Command]\n    pub fn use_in_command(&self, cmd: &mut Command) {\n        if let Some(curent_dir) = &self.current_dir {\n            cmd.current_dir(curent_dir);\n        }\n        for (name, url) in self.override_inputs.iter() {\n            cmd.arg(\"--override-input\").arg(name).arg(url.to_string());\n        }\n        if self.no_write_lock_file {\n            cmd.arg(\"--no-write-lock-file\");\n        }\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/eval.rs",
    "content": "//! Work with `nix eval`\nuse std::process::Stdio;\n\nuse crate::command::{CommandError, NixCmd, NixCmdError};\n\nuse super::{command::FlakeOptions, url::FlakeUrl};\n\n/// Run `nix eval <url> --json` and parse its JSON\npub async fn nix_eval<T>(\n    nixcmd: &NixCmd,\n    opts: &FlakeOptions,\n    url: &FlakeUrl,\n) -> Result<T, NixCmdError>\nwhere\n    T: serde::de::DeserializeOwned,\n{\n    nix_eval_(nixcmd, opts, url, false).await\n}\n\n/// Like [nix_eval] but return `None` if the attribute is missing\npub async fn nix_eval_maybe<T>(\n    cmd: &NixCmd,\n    opts: &FlakeOptions,\n    url: &FlakeUrl,\n) -> Result<Option<T>, NixCmdError>\nwhere\n    T: Default + serde::de::DeserializeOwned,\n{\n    let result = nix_eval_(cmd, opts, url, true).await;\n    match result {\n        Ok(v) => Ok(Some(v)),\n        Err(err) if error_is_missing_attribute(&err) => {\n            Ok(None) // Attr is missing\n        }\n        Err(err) => Err(err),\n    }\n}\n\nasync fn nix_eval_<T>(\n    nixcmd: &NixCmd,\n    opts: &FlakeOptions,\n    url: &FlakeUrl,\n    capture_stderr: bool,\n) -> Result<T, NixCmdError>\nwhere\n    T: serde::de::DeserializeOwned,\n{\n    let stdout = nixcmd\n        .run_with(&[\"eval\"], |cmd| {\n            cmd.stdout(Stdio::piped());\n            if capture_stderr {\n                cmd.stderr(Stdio::piped());\n            }\n            cmd.args([\"--json\"]);\n            opts.use_in_command(cmd);\n            cmd.arg(url.to_string());\n            // Avoid Nix from dumping logs related to `--override-input` use. Yes, this requires *double* use of `--quiet`.\n            cmd.args([\"--quiet\", \"--quiet\"]);\n        })\n        .await?;\n    let v = serde_json::from_slice::<T>(&stdout)?;\n    Ok(v)\n}\n\n/// Check that [NixCmdError] is a missing attribute error\nfn error_is_missing_attribute(err: &NixCmdError) -> bool {\n    if let NixCmdError::CmdError(CommandError::ProcessFailed { stderr, .. }) = err {\n        if stderr.contains(\"does not provide attribute\") {\n            return true;\n        }\n    }\n    false\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/functions/README.md",
    "content": "## Rust + Nix FFI\n\nhttps://github.com/srid/devour-flake introduced the idea of defining \"functions\" in Nix flake, that can be called from any external process. The flake's package derivation acts as the function \"body\", with its `inputs` acting as function \"arguments\"; the built output of that derivation is the function's \"output\".\n\nThis Rust package, `nix_rs::flake::functions`, provides the Rust FFI adapter to work with such Nix functions in Rust, using simpler API. You define your input & output structs in Rust, implement the `FlakeFn` trait and voilà !\n\nIn effect, this generalizes `devour-flake` to be able to define such functions. See `devour_flake.rs` in this repo for an example.\n\n## Inspiration\n\n- [devour-flake](https://github.com/srid/devour-flake): Original use of this pattern.\n- [inspect](https://github.com/DeterminateSystems/inspect) works similar to `devour-flake`, but is tied to flake schemas, and the function body is hardcoded (just as `devour-flake`).\n"
  },
  {
    "path": "crates/nix_rs/src/flake/functions/addstringcontext/flake.nix",
    "content": "{\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixpkgs-unstable\";\n    flake-parts.url = \"github:hercules-ci/flake-parts\";\n    jsonfile = { flake = false; };\n  };\n  outputs = inputs:\n    inputs.flake-parts.lib.mkFlake { inherit inputs; } {\n      systems = [ \"x86_64-linux\" \"aarch64-linux\" \"x86_64-darwin\" \"aarch64-darwin\" ];\n      perSystem = { pkgs, lib, ... }:\n        let\n          json = builtins.fromJSON (builtins.readFile inputs.jsonfile);\n          jsonWithPathContext = lib.flip lib.mapAttrsRecursive json (k: v:\n            if lib.lists.last k == \"outPaths\" || lib.lists.last k == \"allDeps\" then\n              builtins.map (path: builtins.storePath path) v\n            else\n              v\n          );\n        in\n        {\n          packages.default = pkgs.writeText \"addstringcontext.json\" (builtins.toJSON jsonWithPathContext);\n        };\n    };\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/functions/addstringcontext/mod.rs",
    "content": "//! Transform a JSON file with Nix store paths such that the resultant JSON file path will track those paths as dependencies. This requires use of `--impure`.\n///\n/// Only values of keys called `outPaths` in the JSON will be transformed.\n///\n/// https://nix.dev/manual/nix/2.23/language/string-context\nuse super::core::FlakeFn;\nuse crate::{command::NixCmd, flake::url::FlakeUrl};\nuse lazy_static::lazy_static;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::{path::Path, path::PathBuf};\n\nstruct AddStringContextFn;\n\nlazy_static! {\n    /// URL to our flake function\n    static ref FLAKE_ADDSTRINGCONTEXT: FlakeUrl = {\n        let path = env!(\"FLAKE_ADDSTRINGCONTEXT\");\n        Into::<FlakeUrl>::into(Path::new(path)).with_attr(\"default\")\n    };\n}\n\nimpl FlakeFn for AddStringContextFn {\n    type Input = AddStringContextInput;\n    type Output = Value; // We don't care to parse the output\n\n    fn flake() -> &'static FlakeUrl {\n        &FLAKE_ADDSTRINGCONTEXT\n    }\n}\n\n/// Input to FlakeMetadata\n#[derive(Serialize, Deserialize, Debug)]\nstruct AddStringContextInput {\n    /// The JSON file to process\n    jsonfile: FlakeUrl,\n}\n\n/// Add string context to `outPath`s in a JSON file.\n///\n/// Resultant JSON file will track those paths as dependencies. Additionally, an out-link will be created at `out_link` if provided.\npub async fn addstringcontext(\n    cmd: &NixCmd,\n    jsonfile: &Path,\n    out_link: Option<&Path>,\n) -> Result<PathBuf, super::core::Error> {\n    const IMPURE: bool = true; // Our flake.nix uses builtin.storePath\n\n    // We have to use relative paths to avoid a Nix issue on macOS witih /tmp paths.\n    let jsonfile_parent = jsonfile.parent().unwrap();\n    let jsonfile_name = jsonfile.file_name().unwrap().to_string_lossy();\n    let pwd = Some(jsonfile_parent);\n\n    let current_pwd = std::env::current_dir()?;\n    let out_link_absolute: Option<PathBuf> = out_link.map(|p| current_pwd.join(p));\n\n    let input = AddStringContextInput {\n        jsonfile: FlakeUrl(format!(\"path:{}\", jsonfile_name)),\n    };\n    let (path_with_string_context, _json_value) = AddStringContextFn::call(\n        cmd,\n        IMPURE,\n        pwd,\n        out_link_absolute.as_ref().map(PathBuf::as_ref),\n        vec![],\n        input,\n    )\n    .await?;\n    Ok(path_with_string_context)\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/functions/core.rs",
    "content": "//! Flake function trait\nuse crate::{command::NixCmd, flake::url::FlakeUrl};\nuse lazy_static::lazy_static;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::{\n    env,\n    ffi::OsString,\n    os::unix::ffi::OsStringExt,\n    path::{Path, PathBuf},\n    process::Stdio,\n};\n\nlazy_static! {\n    static ref TRUE_FLAKE: FlakeUrl = {\n        let path = env!(\"TRUE_FLAKE\");\n        Into::<FlakeUrl>::into(Path::new(path))\n    };\n    static ref FALSE_FLAKE: FlakeUrl = {\n        let path = env!(\"FALSE_FLAKE\");\n        Into::<FlakeUrl>::into(Path::new(path))\n    };\n}\n\n/// Trait for flake functions\npub trait FlakeFn {\n    /// Input type, corresponding to flake inputs\n    ///\n    /// A field named `flake` will be treated special (extra args' --override-inputs operates on this flake)\n    type Input;\n    /// Output generated by building the flake fn\n    type Output;\n\n    /// Get the flake URL referencing this function\n    fn flake() -> &'static FlakeUrl;\n\n    /// Initialize the type after reading from Nix build\n    fn init(_out: &mut Self::Output) {}\n\n    /// Call the flake function, taking `Self::Input`, returning `Self::Output` along with the built store path output as `PathBuf`.\n    ///\n    /// The store path output can be useful for further processing, if you need it with its entire closure (for e.g., to `nix copy` everything in `Self::Output` at once).\n    ///\n    /// Arguments:\n    /// - `nixcmd`: The Nix command to use\n    /// - `verbose`: Whether to avoid the --override-input noise suppression.\n    /// - `extra_args`: Extra arguments to pass to `nix build`. --override-input is treated specially, to account for the flake input named `flake` (as defined in `Self::Input`)\n    /// - `input`: The input arguments to the flake function.\n    fn call(\n        nixcmd: &NixCmd,\n        // FIXME: Don't do this; instead take dyn trait options\n        impure: bool,\n        pwd: Option<&Path>,\n        m_out_link: Option<&Path>,\n        extra_args: Vec<String>,\n        input: Self::Input,\n    ) -> impl std::future::Future<Output = Result<(PathBuf, Self::Output), Error>> + Send\n    where\n        Self::Input: Serialize + Send + Sync,\n        Self::Output: Sync + for<'de> Deserialize<'de>,\n    {\n        async move {\n            let mut cmd = nixcmd.command(&[\"build\"]);\n            cmd.args([Self::flake(), \"-L\", \"--print-out-paths\"]);\n\n            if impure {\n                cmd.arg(\"--impure\");\n            }\n\n            if let Some(out_link) = m_out_link {\n                cmd.arg(\"--out-link\");\n                cmd.arg(out_link);\n            } else {\n                cmd.arg(\"--no-link\");\n            }\n\n            let input_vec = to_vec(&input);\n            for (k, v) in input_vec {\n                cmd.arg(\"--override-input\");\n                cmd.arg(k);\n                cmd.arg(v);\n            }\n\n            cmd.args(transform_override_inputs(&extra_args));\n\n            if let Some(pwd) = pwd {\n                cmd.current_dir(pwd);\n            }\n\n            crate::command::trace_cmd(&cmd);\n\n            let output_fut = cmd.stdout(Stdio::piped()).spawn()?;\n            let output = output_fut.wait_with_output().await?;\n            if output.status.success() {\n                let store_path =\n                    PathBuf::from(OsString::from_vec(output.stdout.trim_ascii_end().into()));\n                let mut v: Self::Output =\n                    serde_json::from_reader(std::fs::File::open(&store_path)?)?;\n                Self::init(&mut v);\n                Ok((store_path, v))\n            } else {\n                Err(Error::NixBuildFailed(output.status.code()))\n            }\n        }\n    }\n}\n\n/// Transform `--override-input` arguments to use `flake/` prefix, which\n/// devour_flake expects.\n///\n/// NOTE: This assumes that Input struct contains a field named exactly \"flake\" referring to the flake. We should probably be smart about this.\nfn transform_override_inputs(args: &[String]) -> Vec<String> {\n    let mut new_args = Vec::with_capacity(args.len());\n    let mut iter = args.iter().peekable();\n\n    while let Some(arg) = iter.next() {\n        new_args.push(arg.clone());\n        if arg == \"--override-input\" {\n            if let Some(next_arg) = iter.next() {\n                new_args.push(format!(\"flake/{}\", next_arg));\n            }\n        }\n    }\n\n    new_args\n}\n\n/// Convert a struct of uniform value types (Option allowed, however) into a vector of fields. The value should be of String kind.\nfn to_vec<T>(value: &T) -> Vec<(String, String)>\nwhere\n    T: Serialize,\n{\n    let map = serde_json::to_value(value)\n        .unwrap()\n        .as_object()\n        .unwrap_or_else(|| panic!(\"Bad struct for FlakeFn\"))\n        .clone();\n\n    map.into_iter()\n        .filter_map(|(k, v)| match v {\n            Value::String(s) => Some((k, s.to_string())),\n            Value::Bool(b) => Some((\n                k,\n                if b {\n                    TRUE_FLAKE.to_string()\n                } else {\n                    FALSE_FLAKE.to_string()\n                }\n                .to_string(),\n            )),\n            _ => None,\n        })\n        .collect()\n}\n\n/// Errors associated with `FlakeFn::call`\n#[derive(thiserror::Error, Debug)]\npub enum Error {\n    /// IO error\n    #[error(\"IO error: {0}\")]\n    IOError(#[from] std::io::Error),\n\n    /// Non-zero exit code\n    #[error(\"`nix build` failed; exit code: {0:?}\")]\n    NixBuildFailed(Option<i32>),\n\n    /// JSON error\n    #[error(\"JSON error: {0}\")]\n    JSONError(#[from] serde_json::Error),\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/functions/metadata/flake.nix",
    "content": "{\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixpkgs-unstable\";\n    flake-parts.url = \"github:hercules-ci/flake-parts\";\n    flake = { };\n    include-inputs = { };\n  };\n  outputs = inputs:\n    inputs.flake-parts.lib.mkFlake { inherit inputs; } {\n      systems = [ \"x86_64-linux\" \"aarch64-linux\" \"x86_64-darwin\" \"aarch64-darwin\" ];\n      perSystem = { pkgs, lib, ... }:\n        let\n          include-inputs = inputs.include-inputs.value;\n          fn = if include-inputs then \"nix_rs-metadata-full.json\" else \"nix_rs-metadata-flakeonly.json\";\n        in\n        {\n          packages = {\n            default = pkgs.writeText fn (builtins.toJSON {\n              # *All* nested inputs are flattened into a single list of inputs.\n              inputs = if !include-inputs then null else\n              let\n                inputsFor = visited: prefix: f:\n                  let\n                    here = builtins.unsafeDiscardStringContext \"${f.outPath}\";\n                  in\n                  # Keep track of visited nodes to workaround a nasty Nix design wart that leads to infinite recursion otherwise.\n                    # https://github.com/NixOS/nix/issues/7807\n                    # https://github.com/juspay/omnix/pull/389\n                  lib.optionals (!lib.hasAttr here visited)\n                    (lib.concatLists (lib.mapAttrsToList\n                      (k: v: [{ name = \"${prefix}__${k}\"; path = v.outPath; }] ++\n                        (lib.optionals (lib.hasAttr \"inputs\" v))\n                          (inputsFor (visited // { \"${here}\" = true; }) \"${prefix}/${k}\" v))\n                      f.inputs));\n              in\n              inputsFor { } \"flake\" inputs.flake;\n              flake = inputs.flake.outPath;\n            });\n          };\n        };\n    };\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/functions/metadata/mod.rs",
    "content": "//! Retrieve metadata for a flake.\nuse super::core::FlakeFn;\nuse crate::{command::NixCmd, flake::url::FlakeUrl};\nuse lazy_static::lazy_static;\nuse serde::{Deserialize, Serialize};\nuse std::{path::Path, path::PathBuf};\n\n/// Flake metadata computed in Nix.\npub struct FlakeMetadataFn;\n\nlazy_static! {\n    /// URL to our flake function\n    static ref FLAKE_METADATA: FlakeUrl = {\n        let path = env!(\"FLAKE_METADATA\");\n        Into::<FlakeUrl>::into(Path::new(path)).with_attr(\"default\")\n    };\n}\n\nimpl FlakeFn for FlakeMetadataFn {\n    type Input = FlakeMetadataInput;\n    type Output = FlakeMetadata;\n\n    fn flake() -> &'static FlakeUrl {\n        &FLAKE_METADATA\n    }\n}\n\n/// Input to FlakeMetadata\n#[derive(Serialize, Deserialize, Debug)]\npub struct FlakeMetadataInput {\n    /// The flake to operate on\n    pub flake: FlakeUrl,\n\n    /// Included flake inputs transitively in the result\n    ///\n    /// NOTE: This makes evaluation more expensive.\n    #[serde(rename = \"include-inputs\")]\n    pub include_inputs: bool,\n}\n\n/// Flake metadata\n///\n/// See [Nix doc](https://nix.dev/manual/nix/2.18/command-ref/new-cli/nix3-flake-metadata)\n#[derive(Serialize, Deserialize, Debug)]\npub struct FlakeMetadata {\n    /// Store path to this flake\n    pub flake: PathBuf,\n\n    /// Store path to each flake input\n    ///\n    /// Only available if `FlakeInput::include_inputs` is enabled.\n    pub inputs: Option<Vec<FlakeInput>>,\n}\n\n/// A flake input\n#[derive(Serialize, Deserialize, Debug)]\npub struct FlakeInput {\n    /// Unique identifier\n    pub name: String,\n    /// Local path to the input\n    pub path: PathBuf,\n}\n\nimpl FlakeMetadata {\n    /// Get the [FlakeMetadata] for the given flake\n    pub async fn from_nix(\n        cmd: &NixCmd,\n        input: FlakeMetadataInput,\n    ) -> Result<(PathBuf, FlakeMetadata), super::core::Error> {\n        FlakeMetadataFn::call(cmd, false, None, None, vec![], input).await\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/functions/mod.rs",
    "content": "//! Calling Nix functions (defined in a flake) from Rust, as if to provide FFI.\n//\n// This model provides a simpler alternative to Flake Schemas, but it can also do more than Flake Schemas can (such as building derivations).\n\npub mod addstringcontext;\npub mod core;\npub mod metadata;\n"
  },
  {
    "path": "crates/nix_rs/src/flake/mod.rs",
    "content": "//! Rust module for Nix flakes\n\npub mod command;\npub mod eval;\npub mod functions;\npub mod outputs;\npub mod schema;\npub mod system;\npub mod url;\n\nuse schema::FlakeSchemas;\nuse serde::{Deserialize, Serialize};\n\nuse system::System;\nuse tracing::instrument;\n\nuse self::{outputs::FlakeOutputs, url::FlakeUrl};\n\nuse crate::{\n    command::{NixCmd, NixCmdError},\n    config::NixConfig,\n};\n\n/// All the information about a Nix flake\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct Flake {\n    /// The flake url which this struct represents\n    pub url: FlakeUrl,\n    /// Flake outputs derived from [FlakeSchemas]\n    pub output: FlakeOutputs,\n    // TODO: Add `nix flake metadata` info.\n}\n\nimpl Flake {\n    /// Get [Flake] info for the given flake url\n\n    #[instrument(name = \"flake\", skip(nix_cmd))]\n    pub async fn from_nix(\n        nix_cmd: &NixCmd,\n        nix_config: &NixConfig,\n        url: FlakeUrl,\n    ) -> Result<Flake, NixCmdError> {\n        let schemas = FlakeSchemas::from_nix(nix_cmd, &url, &nix_config.system.value).await?;\n        Ok(Flake {\n            url,\n            output: schemas.into(),\n        })\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/outputs.rs",
    "content": "//! Nix flake outputs\n\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\n\nuse super::schema::{FlakeSchemas, Val};\n\n/// Outputs of a flake\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(untagged)]\npub enum FlakeOutputs {\n    /// Terminal value that is not an attrset.\n    Val(Val),\n    /// An attrset of nested [FlakeOutputs]\n    Attrset(HashMap<String, FlakeOutputs>),\n}\n\nimpl FlakeOutputs {\n    /// Get the terminal value\n    pub fn get_val(&self) -> Option<&Val> {\n        match self {\n            Self::Val(v) => Some(v),\n            _ => None,\n        }\n    }\n\n    /// Get the attrset\n    pub fn get_attrset(&self) -> Option<&HashMap<String, FlakeOutputs>> {\n        match self {\n            Self::Val(_) => None,\n            Self::Attrset(map) => Some(map),\n        }\n    }\n\n    /// Get the attrset as a vector of key-value pairs\n    ///\n    /// **NOTE**: Only terminal values are included!\n    pub fn get_attrset_of_val(&self) -> Vec<(String, Val)> {\n        self.get_attrset().map_or(vec![], |map| {\n            map.iter()\n                .filter_map(|(k, v)| v.get_val().map(|val| (k.clone(), val.clone())))\n                .collect()\n        })\n    }\n\n    /// Lookup the given path, returning a reference to the value if it exists.\n    ///\n    /// # Example\n    /// ```no_run\n    /// let tree : &nix_rs::flake::outputs::FlakeOutputs = todo!();\n    /// let val = tree.get_by_path(&[\"aarch64-darwin\", \"default\"]);\n    /// ```\n    pub fn get_by_path(&self, path: &[&str]) -> Option<&Self> {\n        let mut current = self;\n        for key in path {\n            let map = current.get_attrset()?;\n            current = map.get(*key)?;\n        }\n        Some(current)\n    }\n}\n\nimpl From<FlakeSchemas> for FlakeOutputs {\n    fn from(schema: FlakeSchemas) -> Self {\n        schema.to_flake_outputs()\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/schema.rs",
    "content": "//! Nix flake-schemas\n\nuse lazy_static::lazy_static;\nuse serde::{Deserialize, Serialize};\nuse std::{\n    collections::{BTreeMap, HashMap},\n    fmt::Display,\n    path::Path,\n};\n\nuse crate::system_list::SystemsListFlakeRef;\n\nuse super::{command::FlakeOptions, eval::nix_eval, outputs::FlakeOutputs, url::FlakeUrl};\n\nlazy_static! {\n  /// Flake URL of the default flake schemas\n  ///\n  /// We expect this environment to be set in Nix build and shell.\n  pub static ref DEFAULT_FLAKE_SCHEMAS: FlakeUrl = {\n    Into::<FlakeUrl>::into(Path::new(env!(\"DEFAULT_FLAKE_SCHEMAS\")))\n  };\n\n  /// Flake URL of the flake that defines functions for inspecting flake outputs\n  ///\n  /// We expect this environment to be set in Nix build and shell.\n  pub static ref INSPECT_FLAKE: FlakeUrl = {\n    Into::<FlakeUrl>::into(Path::new(env!(\"INSPECT_FLAKE\")))\n  };\n}\n\n/// Represents the schema of a given flake evaluated using [static@INSPECT_FLAKE]\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct FlakeSchemas {\n    /// Each key in the map represents either a top-level flake output or other metadata (e.g. `docs`)\n    pub inventory: HashMap<String, InventoryItem>,\n}\n\n/// A tree-like structure representing each flake output or metadata in [FlakeSchemas]\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(untagged)]\npub enum InventoryItem {\n    /// Represents a terminal node in the tree\n    Leaf(Leaf),\n    /// Represents a non-terminal node in the tree\n    Attrset(HashMap<String, InventoryItem>),\n}\n\nimpl FlakeSchemas {\n    /// Get the [FlakeSchemas] for the given flake\n    ///\n    /// This uses [static@INSPECT_FLAKE] and [static@DEFAULT_FLAKE_SCHEMAS]\n    pub async fn from_nix(\n        nix_cmd: &crate::command::NixCmd,\n        flake_url: &super::url::FlakeUrl,\n        system: &super::System,\n    ) -> Result<Self, crate::command::NixCmdError> {\n        let inspect_flake: FlakeUrl = INSPECT_FLAKE\n            // Why `exculdingOutputPaths`?\n            //   This function is much faster than `includingOutputPaths` and also solves <https://github.com/juspay/omnix/discussions/231>\n            //   Also See: https://github.com/DeterminateSystems/inspect/blob/7f0275abbdc46b3487ca69e2acd932ce666a03ff/flake.nix#L139\n            //\n            //\n            // Note: We might need to use `includingOutputPaths` in the future, when replacing `devour-flake`.\n            // In which case, `om ci` and `om show` can invoke the appropriate function from `INSPECT_FLAKE`.\n            //\n            .with_attr(\"contents.excludingOutputPaths\");\n        let systems_flake = SystemsListFlakeRef::from_known_system(system)\n            // TODO: don't use unwrap\n            .unwrap()\n            .0\n            .clone();\n        let flake_opts = FlakeOptions {\n            no_write_lock_file: true,\n            override_inputs: BTreeMap::from_iter([\n                (\n                    \"flake-schemas\".to_string(),\n                    DEFAULT_FLAKE_SCHEMAS.to_owned(),\n                ),\n                (\"flake\".to_string(), flake_url.clone()),\n                (\"systems\".to_string(), systems_flake),\n            ]),\n            ..Default::default()\n        };\n        let v = nix_eval::<Self>(nix_cmd, &flake_opts, &inspect_flake).await?;\n        Ok(v)\n    }\n\n    /// Convert [FlakeSchemas] to [FlakeOutputs]\n    pub(crate) fn to_flake_outputs(&self) -> FlakeOutputs {\n        FlakeOutputs::Attrset(\n            self.inventory\n                .iter()\n                .filter_map(|(k, v)| Some((k.clone(), v.to_flake_outputs()?)))\n                .collect(),\n        )\n    }\n}\n\nimpl InventoryItem {\n    fn to_flake_outputs(&self) -> Option<FlakeOutputs> {\n        match self {\n            Self::Leaf(leaf) => leaf.get_val().cloned().map(FlakeOutputs::Val),\n            Self::Attrset(map) => {\n                if let Some(children) = map.get(\"children\") {\n                    children.to_flake_outputs()\n                } else {\n                    let filtered: HashMap<_, _> = map\n                        .iter()\n                        .filter_map(|(k, v)| Some((k.clone(), v.to_flake_outputs()?)))\n                        .collect();\n                    if filtered.is_empty() {\n                        None\n                    } else {\n                        Some(FlakeOutputs::Attrset(filtered))\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// A terminal value of a flake schema\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(untagged)]\npub enum Leaf {\n    #[allow(missing_docs)]\n    Val(Val),\n    /// Represents description for a flake output\n    /// (e.g. `Doc` for `formatter` will be \"The `formatter` output specifies the package to use to format the project.\")\n    Doc(String),\n}\n\nimpl Leaf {\n    /// Get the [Val] if any\n    fn get_val(&self) -> Option<&Val> {\n        match self {\n            Self::Val(v) => Some(v),\n            _ => None,\n        }\n    }\n}\n\n/// A terminal value of a flake output\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n#[serde(rename_all = \"camelCase\")]\npub struct Val {\n    #[serde(rename = \"what\")]\n    /// Represents the type of the flake output\n    pub type_: Type,\n    /// If the flake output is a derivation, this will be the name of the derivation\n    pub derivation_name: Option<String>,\n    /// A short description derived from `meta.description` of the derivation with [Val::derivation_name]\n    pub short_description: Option<String>,\n}\n\nimpl Default for Val {\n    fn default() -> Self {\n        Self {\n            type_: Type::Unknown,\n            derivation_name: None,\n            short_description: None,\n        }\n    }\n}\n\n/// The type of a flake output [Val]\n///\n/// These types can differ based on [static@DEFAULT_FLAKE_SCHEMAS].\n/// The types here are based on <https://github.com/DeterminateSystems/flake-schemas>\n/// For example, see [NixosModule type](https://github.com/DeterminateSystems/flake-schemas/blob/0a5c42297d870156d9c57d8f99e476b738dcd982/flake.nix#L268)\n#[allow(missing_docs)]\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum Type {\n    #[serde(rename = \"NixOS module\")]\n    NixosModule,\n    #[serde(rename = \"NixOS configuration\")]\n    NixosConfiguration,\n    #[serde(rename = \"nix-darwin configuration\")]\n    DarwinConfiguration,\n    #[serde(rename = \"package\")]\n    Package,\n    #[serde(rename = \"development environment\")]\n    DevShell,\n    #[serde(rename = \"CI test\")]\n    Check,\n    #[serde(rename = \"app\")]\n    App,\n    #[serde(rename = \"template\")]\n    Template,\n    #[serde(other)]\n    Unknown,\n}\n\nimpl Type {\n    /// Get the icon for this type\n    pub fn to_icon(&self) -> &'static str {\n        match self {\n            Self::NixosModule => \"❄️\",\n            Self::NixosConfiguration => \"🔧\",\n            Self::DarwinConfiguration => \"🍎\",\n            Self::Package => \"📦\",\n            Self::DevShell => \"🐚\",\n            Self::Check => \"🧪\",\n            Self::App => \"📱\",\n            Self::Template => \"🏗️\",\n            Self::Unknown => \"❓\",\n        }\n    }\n}\n\nimpl Display for Type {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_str(&format!(\"{:?}\", self))\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/system.rs",
    "content": "//! Nix system types\nuse std::{\n    convert::Infallible,\n    fmt::{Display, Formatter},\n    str::FromStr,\n};\n\nuse serde::{Deserialize, Serialize};\nuse serde_with::{DeserializeFromStr, SerializeDisplay};\n\n/// The system for which a derivation will build\n///\n/// The enum includes the four standard systems, as well as a fallback to\n/// capture the rest.\n#[derive(\n    Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, SerializeDisplay, DeserializeFromStr,\n)]\npub enum System {\n    /// macOS system\n    Darwin(Arch),\n    /// Linux system\n    Linux(Arch),\n    /// Other system\n    Other(String),\n}\n\n/// CPU architecture in the system\n#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]\npub enum Arch {\n    /// aarch64\n    Aarch64,\n    /// x86_64\n    X86_64,\n}\n\nimpl FromStr for System {\n    type Err = Infallible;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        Ok(Self::from(s))\n    }\n}\n\nimpl From<&str> for System {\n    fn from(s: &str) -> Self {\n        match s {\n            \"aarch64-linux\" => Self::Linux(Arch::Aarch64),\n            \"x86_64-linux\" => Self::Linux(Arch::X86_64),\n            \"x86_64-darwin\" => Self::Darwin(Arch::X86_64),\n            \"aarch64-darwin\" => Self::Darwin(Arch::Aarch64),\n            _ => Self::Other(s.to_string()),\n        }\n    }\n}\n\nimpl From<String> for System {\n    fn from(s: String) -> Self {\n        Self::from(s.as_str())\n    }\n}\n\nimpl AsRef<str> for System {\n    fn as_ref(&self) -> &str {\n        match self {\n            System::Linux(Arch::Aarch64) => \"aarch64-linux\",\n            System::Linux(Arch::X86_64) => \"x86_64-linux\",\n            System::Darwin(Arch::X86_64) => \"x86_64-darwin\",\n            System::Darwin(Arch::Aarch64) => \"aarch64-darwin\",\n            System::Other(s) => s,\n        }\n    }\n}\n\nimpl From<System> for String {\n    fn from(s: System) -> Self {\n        s.as_ref().to_string()\n    }\n}\n\nimpl Display for System {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.as_ref())\n    }\n}\n\nimpl System {\n    /// Return the human readable title for the Nix system\n    pub fn human_readable(&self) -> String {\n        match self {\n            System::Linux(arch) => format!(\"Linux ({})\", arch.human_readable()),\n            System::Darwin(arch) => format!(\"macOS ({})\", arch.human_readable()),\n            System::Other(s) => s.clone(),\n        }\n    }\n}\n\nimpl Arch {\n    /// Return the human readable title for the CPU architecture\n    pub fn human_readable(&self) -> &'static str {\n        match self {\n            Self::Aarch64 => \"ARM\",\n            Self::X86_64 => \"Intel\",\n        }\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/url/attr.rs",
    "content": "//! Work with flake attributes\nuse serde::{Deserialize, Serialize};\n\n/// The (optional) attribute output part of a [super::FlakeUrl]\n///\n/// Example: `foo` in `.#foo`.\n#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]\npub struct FlakeAttr(pub Option<String>);\n\nimpl FlakeAttr {\n    /// Create a new [FlakeAttr]\n    pub fn new(attr: &str) -> Self {\n        FlakeAttr(Some(attr.to_owned()))\n    }\n\n    /// A missing flake attribute\n    pub fn none() -> Self {\n        FlakeAttr(None)\n    }\n\n    /// Get the attribute name.\n    ///\n    /// If no such attribute exists, return \"default\".\n    pub fn get_name(&self) -> String {\n        self.0.clone().unwrap_or_else(|| \"default\".to_string())\n    }\n\n    /// Whether an explicit attribute is not set\n    pub fn is_none(&self) -> bool {\n        self.0.is_none()\n    }\n\n    /// Return nested attrs if the user specified one is separated by '.'\n    pub fn as_list(&self) -> Vec<String> {\n        self.0\n            .clone()\n            .map(|s| s.split('.').map(|s| s.to_string()).collect())\n            .unwrap_or_default()\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/url/core.rs",
    "content": "//! Flake URL types\n//!\n//! See <https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html#url-like-syntax>\nuse std::{\n    fmt::{Display, Formatter},\n    ops::Deref,\n    path::{Path, PathBuf},\n    str::FromStr,\n};\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::{\n    command::NixCmd,\n    flake::functions::metadata::{FlakeMetadata, FlakeMetadataInput},\n};\n\nuse super::attr::FlakeAttr;\n\n/// A flake URL\n///\n/// See [syntax here](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html#url-like-syntax).\n///\n/// Use `FromStr` to parse a string into a `FlakeUrl`. Or `From` or `Into` if\n/// you know the URL is valid.\n#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]\npub struct FlakeUrl(pub String);\n\nimpl AsRef<str> for FlakeUrl {\n    fn as_ref(&self) -> &str {\n        &self.0\n    }\n}\n\nimpl Deref for FlakeUrl {\n    type Target = str;\n\n    fn deref(&self) -> &Self::Target {\n        &self.0\n    }\n}\n\nimpl FlakeUrl {\n    /// Return the local path if the flake URL is a local path\n    ///\n    /// Applicable only if the flake URL uses the [Path-like\n    /// syntax](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html#path-like-syntax)\n    pub fn as_local_path(&self) -> Option<&Path> {\n        let s = self.0.strip_prefix(\"path:\").unwrap_or(&self.0);\n        if s.starts_with('.') || s.starts_with('/') {\n            // Strip query (`?..`) and attrs (`#..`)\n            let s = s.split('?').next().unwrap_or(s);\n            let s = s.split('#').next().unwrap_or(s);\n            Some(Path::new(s))\n        } else {\n            None\n        }\n    }\n\n    /// Return the flake as local path. If the flake is a remote reference, catch it to local Nix store first.\n    pub async fn as_local_path_or_fetch(\n        &self,\n        cmd: &NixCmd,\n    ) -> Result<PathBuf, crate::flake::functions::core::Error> {\n        if let Some(path) = self.as_local_path() {\n            Ok(path.to_path_buf())\n        } else {\n            let (_, meta) = FlakeMetadata::from_nix(\n                cmd,\n                FlakeMetadataInput {\n                    flake: self.clone(),\n                    include_inputs: false, // Don't care about inputs\n                },\n            )\n            .await?;\n            Ok(meta.flake)\n        }\n    }\n\n    /// Split the [super::attr::FlakeAttr] out of the [FlakeUrl]\n    pub fn split_attr(&self) -> (Self, FlakeAttr) {\n        match self.0.split_once('#') {\n            Some((url, attr)) => (FlakeUrl(url.to_string()), FlakeAttr(Some(attr.to_string()))),\n            None => (self.clone(), FlakeAttr(None)),\n        }\n    }\n\n    /// Return the [super::attr::FlakeAttr] of the [FlakeUrl]\n    pub fn get_attr(&self) -> FlakeAttr {\n        self.split_attr().1\n    }\n\n    /// Return the flake URL without the attribute\n    pub fn without_attr(&self) -> Self {\n        let (url, _) = self.split_attr();\n        url\n    }\n\n    /// Return the flake URL with the given attribute\n    pub fn with_attr(&self, attr: &str) -> Self {\n        let (url, _) = self.split_attr();\n        FlakeUrl(format!(\"{}#{}\", url.0, attr))\n    }\n\n    /// Return the flake URL pointing to the sub-flake\n    pub fn sub_flake_url(&self, dir: String) -> FlakeUrl {\n        if dir == \".\" {\n            self.clone()\n        } else if let Some(path) = self.as_local_path() {\n            // Local path; just join the dir\n            let path_with_dir = path.join(dir);\n            FlakeUrl::from(path_with_dir)\n        } else {\n            // Non-path URL; append `dir` query parameter\n            let mut url = self.0.clone();\n            if url.contains('?') {\n                url.push_str(\"&dir=\");\n            } else {\n                url.push_str(\"?dir=\");\n            }\n            url.push_str(&dir);\n            FlakeUrl(url)\n        }\n    }\n}\n\nimpl From<PathBuf> for FlakeUrl {\n    fn from(path: PathBuf) -> Self {\n        FlakeUrl::from(path.as_ref())\n    }\n}\n\nimpl From<&Path> for FlakeUrl {\n    fn from(path: &Path) -> Self {\n        // We do not use `path:` here, because that will trigger copying to the Nix store.\n        FlakeUrl(format!(\"{}\", path.display()))\n    }\n}\n\nimpl FromStr for FlakeUrl {\n    type Err = FlakeUrlError;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        let s = s.trim();\n        if s.is_empty() {\n            Err(FlakeUrlError::Empty)\n        } else {\n            Ok(FlakeUrl(s.to_string()))\n        }\n    }\n}\n\n/// Error type for parsing a [FlakeUrl]\n#[derive(thiserror::Error, Debug)]\npub enum FlakeUrlError {\n    /// Empty string is not a valid Flake URL\n    #[error(\"Empty string is not a valid Flake URL\")]\n    Empty,\n}\n\nimpl Display for FlakeUrl {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.0)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_flake_url_and_attr() {\n        let url = FlakeUrl(\"github:srid/nixci\".to_string());\n        assert_eq!(url.split_attr(), (url.clone(), FlakeAttr(None)));\n        assert_eq!(url.split_attr().1.as_list(), [] as [&str; 0]);\n\n        let url = FlakeUrl(\"github:srid/nixci#extra-tests\".to_string());\n        assert_eq!(\n            url.split_attr(),\n            (\n                FlakeUrl(\"github:srid/nixci\".to_string()),\n                FlakeAttr(Some(\"extra-tests\".to_string()))\n            )\n        );\n        assert_eq!(\n            url.split_attr().1.as_list(),\n            vec![\"extra-tests\".to_string()]\n        );\n\n        let url = FlakeUrl(\".#foo.bar.qux\".to_string());\n        assert_eq!(\n            url.split_attr(),\n            (\n                FlakeUrl(\".\".to_string()),\n                FlakeAttr(Some(\"foo.bar.qux\".to_string()))\n            )\n        );\n        assert_eq!(\n            url.split_attr().1.as_list(),\n            vec![\"foo\".to_string(), \"bar\".to_string(), \"qux\".to_string()]\n        )\n    }\n\n    #[test]\n    fn test_as_local_path() {\n        let url = FlakeUrl(\"github:srid/nixci\".to_string());\n        assert_eq!(url.as_local_path(), None);\n\n        let url = FlakeUrl(\".\".to_string());\n        assert_eq!(url.as_local_path().map(|p| p.to_str().unwrap()), Some(\".\"));\n\n        let url = FlakeUrl(\"/foo\".to_string());\n        assert_eq!(url.as_local_path(), Some(std::path::Path::new(\"/foo\")));\n\n        let url = FlakeUrl(\"./foo?q=bar\".to_string());\n        assert_eq!(url.as_local_path(), Some(std::path::Path::new(\"./foo\")));\n\n        let url = FlakeUrl(\"./foo#attr\".to_string());\n        assert_eq!(url.as_local_path(), Some(std::path::Path::new(\"./foo\")));\n\n        let url = FlakeUrl(\"/foo?q=bar#attr\".to_string());\n        assert_eq!(url.as_local_path(), Some(std::path::Path::new(\"/foo\")));\n\n        let url = FlakeUrl(\"path:.\".to_string());\n        assert_eq!(url.as_local_path(), Some(std::path::Path::new(\".\")));\n\n        let url = FlakeUrl(\"path:./foo\".to_string());\n        assert_eq!(url.as_local_path(), Some(std::path::Path::new(\"./foo\")));\n\n        let url = FlakeUrl(\"path:./foo?q=bar\".to_string());\n        assert_eq!(url.as_local_path(), Some(std::path::Path::new(\"./foo\")));\n\n        let url = FlakeUrl(\"path:./foo#attr\".to_string());\n        assert_eq!(url.as_local_path(), Some(std::path::Path::new(\"./foo\")));\n\n        let url = FlakeUrl(\"path:/foo?q=bar#attr\".to_string());\n        assert_eq!(url.as_local_path(), Some(std::path::Path::new(\"/foo\")));\n\n        /* FIXME!\n        let url = FlakeUrl(\"/project?dir=bar\".to_string());\n        assert_eq!(\n            url.as_local_path(),\n            Some(std::path::Path::new(\"/project/bar\"))\n        );\n        */\n    }\n\n    #[test]\n    fn test_sub_flake_url() {\n        // Path refs\n        let url = FlakeUrl(\".\".to_string());\n        assert_eq!(url.sub_flake_url(\".\".to_string()), url.clone());\n        assert_eq!(\n            url.sub_flake_url(\"sub\".to_string()),\n            FlakeUrl(\"./sub\".to_string())\n        );\n\n        // URI refs\n        let url = FlakeUrl(\"github:srid/nixci\".to_string());\n        assert_eq!(url.sub_flake_url(\".\".to_string()), url.clone());\n        assert_eq!(\n            url.sub_flake_url(\"dev\".to_string()),\n            FlakeUrl(\"github:srid/nixci?dir=dev\".to_string())\n        );\n    }\n\n    #[test]\n    fn test_sub_flake_url_with_query() {\n        let url = FlakeUrl(\"git+https://example.org/my/repo?ref=master\".to_string());\n        assert_eq!(url.sub_flake_url(\".\".to_string()), url.clone());\n        assert_eq!(\n            url.sub_flake_url(\"dev\".to_string()),\n            FlakeUrl(\"git+https://example.org/my/repo?ref=master&dir=dev\".to_string())\n        );\n    }\n\n    #[test]\n    fn test_with_attr() {\n        let url = FlakeUrl(\"github:srid/nixci\".to_string());\n        assert_eq!(\n            url.with_attr(\"foo\"),\n            FlakeUrl(\"github:srid/nixci#foo\".to_string())\n        );\n\n        let url: FlakeUrl = \"github:srid/nixci#foo\".parse().unwrap();\n        assert_eq!(\n            url.with_attr(\"bar\"),\n            FlakeUrl(\"github:srid/nixci#bar\".to_string())\n        );\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/flake/url/mod.rs",
    "content": "//! Work with flake URLs\npub mod attr;\nmod core;\n\npub use core::*;\n"
  },
  {
    "path": "crates/nix_rs/src/info.rs",
    "content": "//! Information about the user's Nix installation\nuse serde::{Deserialize, Serialize};\nuse tokio::sync::OnceCell;\n\nuse crate::{config::NixConfig, env::NixEnv, version::NixVersion};\n\n/// All the information about the user's Nix installation\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct NixInfo {\n    /// Nix version string\n    pub nix_version: NixVersion,\n    /// nix.conf configuration\n    pub nix_config: NixConfig,\n    /// Environment in which Nix was installed\n    pub nix_env: NixEnv,\n}\n\nstatic NIX_INFO: OnceCell<Result<NixInfo, NixInfoError>> = OnceCell::const_new();\n\nimpl NixInfo {\n    /// Get the once version  of `NixInfo`\n    pub async fn get() -> &'static Result<NixInfo, NixInfoError> {\n        NIX_INFO\n            .get_or_init(|| async {\n                let nix_version = NixVersion::get().await.as_ref()?;\n                let nix_config = NixConfig::get().await.as_ref()?;\n                let info = NixInfo::new(*nix_version, nix_config.clone()).await?;\n                Ok(info)\n            })\n            .await\n    }\n\n    /// Determine [NixInfo] on the user's system\n    pub async fn new(\n        nix_version: NixVersion,\n        nix_config: NixConfig,\n    ) -> Result<NixInfo, NixInfoError> {\n        let nix_env = NixEnv::detect().await?;\n        Ok(NixInfo {\n            nix_version,\n            nix_config,\n            nix_env,\n        })\n    }\n}\n\n/// Error type for [NixInfo]\n#[derive(thiserror::Error, Debug)]\npub enum NixInfoError {\n    /// A [crate::command::NixCmdError]\n    #[error(\"Nix command error: {0}\")]\n    NixCmdError(#[from] crate::command::NixCmdError),\n\n    /// A [crate::command::NixCmdError] with a static lifetime\n    #[error(\"Nix command error: {0}\")]\n    NixCmdErrorStatic(#[from] &'static crate::command::NixCmdError),\n\n    /// A [crate::env::NixEnvError]\n    #[error(\"Nix environment error: {0}\")]\n    NixEnvError(#[from] crate::env::NixEnvError),\n\n    /// A [crate::config::NixConfigError]\n    #[error(\"Nix config error: {0}\")]\n    NixConfigError(#[from] &'static crate::config::NixConfigError),\n}\n"
  },
  {
    "path": "crates/nix_rs/src/lib.rs",
    "content": "//! Rust crate to interact with Nix\n//!\n//! This crate exposes various types representing what nix command gives us,\n//! along with a `from_nix` command to evaluate them.\n#![warn(missing_docs)]\npub mod arg;\npub mod command;\npub mod config;\npub mod copy;\npub mod detsys_installer;\npub mod env;\npub mod flake;\npub mod info;\npub mod refs;\npub mod store;\npub mod system_list;\npub mod version;\npub mod version_spec;\n"
  },
  {
    "path": "crates/nix_rs/src/refs.rs",
    "content": "//! Links to Nix manual and other documentation\n\n/// Link to information about the various Nix versions\npub const RELEASE_HISTORY: &str =\n    \"https://nixos.org/manual/nix/stable/release-notes/release-notes.html\";\n"
  },
  {
    "path": "crates/nix_rs/src/store/command.rs",
    "content": "//! Rust wrapper for `nix-store`\nuse std::path::{Path, PathBuf};\n\nuse crate::command::{CommandError, NixCmdError};\nuse serde::{Deserialize, Serialize};\nuse tempfile::TempDir;\nuse thiserror::Error;\nuse tokio::process::Command;\n\nuse super::path::StorePath;\n\n/// The `nix-store` command\n/// See documentation for [nix-store](https://nixos.org/manual/nix/stable/command-ref/nix-store.html)\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]\npub struct NixStoreCmd;\n\nimpl NixStoreCmd {\n    /// Get the associated [Command]\n    pub fn command(&self) -> Command {\n        let mut cmd = Command::new(\"nix-store\");\n        cmd.kill_on_drop(true);\n        cmd\n    }\n}\n\nimpl NixStoreCmd {\n    /// Fetch all build and runtime dependencies of given derivation outputs.\n    ///\n    /// This is done by querying the deriver of each derivation output\n    /// using [NixStoreCmd::nix_store_query_deriver] and then querying all\n    /// dependencies of each deriver using\n    /// [NixStoreCmd::nix_store_query_requisites_with_outputs].  Finally, all\n    /// dependencies of each deriver are collected and returned as\n    /// `Vec<StorePath>`.\n    pub async fn fetch_all_deps(\n        &self,\n        out_paths: &[StorePath],\n    ) -> Result<Vec<StorePath>, NixStoreCmdError> {\n        let all_drvs = self.nix_store_query_deriver(out_paths).await?;\n        let all_outs = self\n            .nix_store_query_requisites_with_outputs(&all_drvs)\n            .await?;\n        Ok(all_outs)\n    }\n\n    /// Return the derivations used to build the given build output.\n    pub async fn nix_store_query_deriver(\n        &self,\n        out_paths: &[StorePath],\n    ) -> Result<Vec<PathBuf>, NixStoreCmdError> {\n        let mut cmd = self.command();\n        cmd.args([\"--query\", \"--valid-derivers\"])\n            .args(out_paths.iter().map(StorePath::as_path));\n\n        let stdout = run_awaiting_stdout(&mut cmd).await?;\n        let drv_paths: Vec<PathBuf> = String::from_utf8(stdout)?\n            .lines()\n            .map(PathBuf::from)\n            .collect();\n        if drv_paths.contains(&PathBuf::from(\"unknown-deriver\")) {\n            return Err(NixStoreCmdError::UnknownDeriver);\n        }\n        Ok(drv_paths)\n    }\n\n    /// Given the derivation paths, this function recursively queries and return all\n    /// of its dependencies in the Nix store.\n    pub async fn nix_store_query_requisites_with_outputs(\n        &self,\n        drv_paths: &[PathBuf],\n    ) -> Result<Vec<StorePath>, NixStoreCmdError> {\n        let mut cmd = self.command();\n        cmd.args([\"--query\", \"--requisites\", \"--include-outputs\"])\n            .args(drv_paths);\n\n        let stdout = run_awaiting_stdout(&mut cmd).await?;\n        Ok(String::from_utf8(stdout)?\n            .lines()\n            .map(|line| StorePath::new(PathBuf::from(line)))\n            .collect())\n    }\n\n    /// Create a file in the Nix store such that it escapes garbage collection.\n    ///\n    /// Return the nix store path added.\n    pub async fn add_file_permanently(\n        &self,\n        symlink: &Path,\n        contents: &str,\n    ) -> Result<StorePath, NixStoreCmdError> {\n        let temp_dir = TempDir::with_prefix(\"omnix-ci-\")?;\n        let temp_file = temp_dir.path().join(\"om.json\");\n        std::fs::write(&temp_file, contents)?;\n\n        let path = self.nix_store_add(&temp_file).await?;\n        self.nix_store_add_root(symlink, &[&path]).await?;\n        Ok(path)\n    }\n\n    /// Run `nix-store --add` on the give path and return the store path added.\n    pub async fn nix_store_add(&self, path: &Path) -> Result<StorePath, NixStoreCmdError> {\n        let mut cmd = self.command();\n        cmd.arg(\"--add\");\n\n        // nix-store is unable to accept absolute paths if it involves a symlink\n        // https://github.com/juspay/omnix/issues/363\n        // To workaround this, we pass the file directly.\n        if let Some(parent) = path.parent() {\n            cmd.current_dir(parent);\n            cmd.arg(path.file_name().unwrap());\n        } else {\n            cmd.arg(path);\n        }\n\n        let stdout = run_awaiting_stdout(&mut cmd).await?;\n        Ok(StorePath::new(PathBuf::from(\n            String::from_utf8(stdout)?.trim_end(),\n        )))\n    }\n\n    /// Run `nix-store --add-root` on the given paths and return the store path added.\n    pub async fn nix_store_add_root(\n        &self,\n        symlink: &Path,\n        paths: &[&StorePath],\n    ) -> Result<(), NixStoreCmdError> {\n        let mut cmd = self.command();\n        cmd.arg(\"--add-root\")\n            .arg(symlink)\n            .arg(\"--realise\")\n            .args(paths);\n\n        run_awaiting_stdout(&mut cmd).await?;\n        Ok(())\n    }\n}\n\nasync fn run_awaiting_stdout(cmd: &mut Command) -> Result<Vec<u8>, NixStoreCmdError> {\n    crate::command::trace_cmd(cmd);\n    let out = cmd.output().await?;\n    if out.status.success() {\n        Ok(out.stdout)\n    } else {\n        let stderr = String::from_utf8_lossy(&out.stderr).to_string();\n        let exit_code = out.status.code();\n        Err(CommandError::ProcessFailed { stderr, exit_code }.into())\n    }\n}\n\n/// `nix-store` command errors\n#[derive(Error, Debug)]\npub enum NixStoreCmdError {\n    /// A [NixCmdError]\n    #[error(transparent)]\n    NixCmdError(#[from] NixCmdError),\n\n    /// nix-store returned \"unknown-deriver\"\n    #[error(\"Unknown deriver\")]\n    UnknownDeriver,\n}\n\nimpl From<std::io::Error> for NixStoreCmdError {\n    fn from(err: std::io::Error) -> Self {\n        let cmd_error: CommandError = err.into();\n        cmd_error.into()\n    }\n}\n\nimpl From<std::string::FromUtf8Error> for NixStoreCmdError {\n    fn from(err: std::string::FromUtf8Error) -> Self {\n        let cmd_error: CommandError = err.into();\n        cmd_error.into()\n    }\n}\n\nimpl From<CommandError> for NixStoreCmdError {\n    fn from(err: CommandError) -> Self {\n        let nixcmd_error: NixCmdError = err.into();\n        nixcmd_error.into()\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/store/mod.rs",
    "content": "//! Dealing with the Nix store\npub mod command;\npub mod path;\npub mod uri;\n"
  },
  {
    "path": "crates/nix_rs/src/store/path.rs",
    "content": "//! Store path management\nuse std::{\n    convert::Infallible,\n    fmt,\n    path::{Path, PathBuf},\n    str::FromStr,\n};\n\nuse serde_with::{DeserializeFromStr, SerializeDisplay};\n\n/// Represents a path in the Nix store, see: <https://zero-to-nix.com/concepts/nix-store#store-paths>\n#[derive(\n    Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Hash, DeserializeFromStr, SerializeDisplay,\n)]\npub enum StorePath {\n    /// Derivation path (ends with `.drv`).\n    Drv(PathBuf),\n    /// Other paths in the Nix store, such as build outputs.\n    /// This won't be a derivation path.\n    Other(PathBuf),\n}\n\nimpl FromStr for StorePath {\n    type Err = Infallible;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        Ok(StorePath::new(PathBuf::from(s)))\n    }\n}\n\nimpl AsRef<Path> for StorePath {\n    fn as_ref(&self) -> &Path {\n        self.as_path().as_ref()\n    }\n}\n\nimpl AsRef<std::ffi::OsStr> for StorePath {\n    fn as_ref(&self) -> &std::ffi::OsStr {\n        self.as_path().as_os_str()\n    }\n}\n\nimpl StorePath {\n    /// Create a new `StorePath` from the given path\n    pub fn new(path: PathBuf) -> Self {\n        if path.ends_with(\".drv\") {\n            StorePath::Drv(path)\n        } else {\n            StorePath::Other(path)\n        }\n    }\n\n    /// Drop store path type distinction, returning the inner path.\n    pub fn as_path(&self) -> &PathBuf {\n        match self {\n            StorePath::Drv(p) => p,\n            StorePath::Other(p) => p,\n        }\n    }\n}\n\nimpl fmt::Display for StorePath {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}\", self.as_path().display())\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/store/uri.rs",
    "content": "//! Store URI management\nuse std::{fmt, str::FromStr};\n\nuse serde::{Deserialize, Serialize};\nuse thiserror::Error;\nuse url::Url;\n\n/// Refers to a Nix store somewhere.\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum StoreURI {\n    /// Nix store accessible over SSH.\n    SSH(SSHStoreURI, Opts),\n}\n\n/// User passed options for a store URI\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct Opts {\n    /// Whether to copy all flake inputs recursively\n    ///\n    /// If disabled, we copy only the flake source itself. Enabling this option is useful when there are private Git inputs but the target machine does not have access to them.\n    #[serde(rename = \"copy-inputs\", default = \"bool::default\")]\n    pub copy_inputs: bool,\n}\n\n/// Remote SSH store URI\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct SSHStoreURI {\n    /// SSH user\n    pub user: Option<String>,\n    /// SSH host\n    pub host: String,\n}\n\n/// Error parsing a store URI\n#[derive(Error, Debug)]\npub enum StoreURIParseError {\n    /// Parse error\n    #[error(transparent)]\n    ParseError(#[from] url::ParseError),\n    /// Unsupported scheme\n    #[error(\"Unsupported scheme: {0}\")]\n    UnsupportedScheme(String),\n    /// Missing host\n    #[error(\"Missing host\")]\n    MissingHost,\n    /// Query string parse error\n    #[error(transparent)]\n    QueryParseError(#[from] serde_qs::Error),\n}\n\nimpl StoreURI {\n    /// Parse a Nix store URI\n    ///\n    /// Currently only supports `ssh` scheme\n    pub fn parse(uri: &str) -> Result<Self, StoreURIParseError> {\n        let url = Url::parse(uri)?;\n        match url.scheme() {\n            \"ssh\" => {\n                let host = url\n                    .host_str()\n                    .ok_or(StoreURIParseError::MissingHost)?\n                    .to_string();\n                let user = if !url.username().is_empty() {\n                    Some(url.username().to_string())\n                } else {\n                    None\n                };\n                let opts = serde_qs::from_str(url.query().unwrap_or(\"\"))?;\n                let ssh_uri = SSHStoreURI { user, host };\n                let store_uri = StoreURI::SSH(ssh_uri, opts);\n                Ok(store_uri)\n            }\n            // Add future schemes here\n            scheme => Err(StoreURIParseError::UnsupportedScheme(scheme.to_string())),\n        }\n    }\n\n    /// Get the options for this store URI\n    pub fn get_options(&self) -> &Opts {\n        match self {\n            StoreURI::SSH(_, opts) => opts,\n        }\n    }\n}\n\nimpl FromStr for StoreURI {\n    type Err = StoreURIParseError;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        StoreURI::parse(s)\n    }\n}\n\nimpl fmt::Display for SSHStoreURI {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        if let Some(user) = &self.user {\n            write!(f, \"{}@{}\", user, self.host)\n        } else {\n            write!(f, \"{}\", self.host)\n        }\n    }\n}\nimpl fmt::Display for StoreURI {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            StoreURI::SSH(uri, _opts) => {\n                // This should construct a valid store URI.\n                write!(f, \"ssh://{}\", uri)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/nix_rs/src/system_list.rs",
    "content": "//! Dealing with system lists\nuse std::{collections::HashMap, convert::Infallible, str::FromStr};\n\nuse crate::{\n    command::{NixCmd, NixCmdError},\n    flake::{system::System, url::FlakeUrl},\n};\nuse lazy_static::lazy_static;\n\nlazy_static! {\n    /// Builtin list of [SystemsListFlakeRef]\n    pub static ref NIX_SYSTEMS: HashMap<String, FlakeUrl> = {\n        serde_json::from_str(env!(\"NIX_SYSTEMS\")).unwrap()\n    };\n}\n\n/// A flake referencing a [SystemsList]\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct SystemsListFlakeRef(pub FlakeUrl);\n\nimpl SystemsListFlakeRef {\n    /// Lookup a known [SystemsListFlakeRef] that will not require network calls\n    pub fn from_known_system(system: &System) -> Option<Self> {\n        NIX_SYSTEMS\n            .get(&system.to_string())\n            .map(|url| SystemsListFlakeRef(url.clone()))\n    }\n}\n\nimpl FromStr for SystemsListFlakeRef {\n    type Err = Infallible;\n    fn from_str(s: &str) -> Result<SystemsListFlakeRef, Infallible> {\n        let system = System::from(s);\n        match SystemsListFlakeRef::from_known_system(&system) {\n            Some(url) => Ok(url),\n            None => Ok(SystemsListFlakeRef(FlakeUrl(s.to_string()))),\n        }\n    }\n}\n\n/// A list of [System]s\npub struct SystemsList(pub Vec<System>);\n\nimpl SystemsList {\n    /// Load the list of systems from a [SystemsListFlakeRef]\n    pub async fn from_flake(cmd: &NixCmd, url: &SystemsListFlakeRef) -> Result<Self, NixCmdError> {\n        // Nix eval, and then return the systems\n        match SystemsList::from_known_flake(url) {\n            Some(systems) => Ok(systems),\n            None => SystemsList::from_remote_flake(cmd, url).await,\n        }\n    }\n\n    async fn from_remote_flake(\n        cmd: &NixCmd,\n        url: &SystemsListFlakeRef,\n    ) -> Result<Self, NixCmdError> {\n        let systems = nix_import_flake::<Vec<System>>(cmd, &url.0).await?;\n        Ok(SystemsList(systems))\n    }\n\n    /// Handle known repos of <https://github.com/nix-systems> thereby avoiding\n    /// network calls.\n    fn from_known_flake(url: &SystemsListFlakeRef) -> Option<Self> {\n        let system = NIX_SYSTEMS\n            .iter()\n            .find_map(|(v, u)| if u == &url.0 { Some(v) } else { None })?;\n        Some(SystemsList(vec![system.clone().into()]))\n    }\n}\n\n/// Evaluate `import <flake-url>` and return the result JSON parsed.\nasync fn nix_import_flake<T>(cmd: &NixCmd, url: &FlakeUrl) -> Result<T, NixCmdError>\nwhere\n    T: Default + serde::de::DeserializeOwned,\n{\n    let flake_path =\n        nix_eval_impure_expr::<String>(cmd, format!(\"builtins.getFlake \\\"{}\\\"\", url.0)).await?;\n    let v = nix_eval_impure_expr(cmd, format!(\"import {}\", flake_path)).await?;\n    Ok(v)\n}\n\nasync fn nix_eval_impure_expr<T>(cmd: &NixCmd, expr: String) -> Result<T, NixCmdError>\nwhere\n    T: Default + serde::de::DeserializeOwned,\n{\n    let v = cmd\n        .run_with_args_expecting_json::<T>(&[\"eval\"], &[\"--impure\", \"--json\", \"--expr\", &expr])\n        .await?;\n    Ok(v)\n}\n"
  },
  {
    "path": "crates/nix_rs/src/version.rs",
    "content": "//! Rust module for `nix --version`\nuse regex::Regex;\nuse serde_with::{DeserializeFromStr, SerializeDisplay};\nuse std::{fmt, str::FromStr};\nuse thiserror::Error;\nuse tokio::sync::OnceCell;\n\nuse tracing::instrument;\n\nuse crate::command::{NixCmd, NixCmdError};\n\n/// Nix version as parsed from `nix --version`\n#[derive(Clone, Copy, PartialOrd, PartialEq, Eq, Debug, SerializeDisplay, DeserializeFromStr)]\npub struct NixVersion {\n    /// Major version\n    pub major: u32,\n    /// Minor version\n    pub minor: u32,\n    /// Patch version\n    pub patch: u32,\n}\n\n/// Error type for parsing `nix --version`\n#[derive(Error, Debug, Clone, PartialEq)]\npub enum BadNixVersion {\n    /// Regex error\n    #[error(\"Regex error: {0}\")]\n    Regex(#[from] regex::Error),\n\n    /// Parse error\n    #[error(\"Parse error (regex): `nix --version` cannot be parsed\")]\n    Parse(#[from] std::num::ParseIntError),\n\n    /// Command error\n    #[error(\"Parse error (int): `nix --version` cannot be parsed\")]\n    Command,\n}\n\nimpl FromStr for NixVersion {\n    type Err = BadNixVersion;\n\n    /// Parse the string output of `nix --version` into a [NixVersion]\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        // NOTE: The parser is lenient in allowing pure nix version (produced\n        // by [Display] instance), so as to work with serde_with instances.\n        let re = Regex::new(r\"(?:nix \\(Nix\\) )?(\\d+)\\.(\\d+)\\.(\\d+)(?:\\+(\\d+))?$\")?;\n\n        let captures = re.captures(s).ok_or(BadNixVersion::Command)?;\n        let major = captures[1].parse::<u32>()?;\n        let minor = captures[2].parse::<u32>()?;\n        let patch = captures[3].parse::<u32>()?;\n\n        Ok(NixVersion {\n            major,\n            minor,\n            patch,\n        })\n    }\n}\n\nstatic NIX_VERSION: OnceCell<Result<NixVersion, NixCmdError>> = OnceCell::const_new();\n\nimpl NixVersion {\n    /// Get the once version of `NixVersion`.\n    #[instrument(name = \"show-config(once)\")]\n    pub async fn get() -> &'static Result<NixVersion, NixCmdError> {\n        NIX_VERSION\n            .get_or_init(|| async {\n                let cmd = NixCmd::default();\n                let nix_ver = NixVersion::from_nix(&cmd).await?;\n                Ok(nix_ver)\n            })\n            .await\n    }\n    /// Get the output of `nix --version`\n    #[instrument(name = \"version\")]\n    pub async fn from_nix(cmd: &NixCmd) -> Result<NixVersion, super::command::NixCmdError> {\n        let v = cmd\n            .run_with_args_expecting_fromstr(&[], &[\"--version\"])\n            .await?;\n        Ok(v)\n    }\n}\n/// The String view for [NixVersion]\nimpl fmt::Display for NixVersion {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}.{}.{}\", self.major, self.minor, self.patch)\n    }\n}\n\n#[tokio::test]\nasync fn test_run_nix_version() {\n    let nix_version = NixVersion::from_nix(&NixCmd::default()).await.unwrap();\n    println!(\"Nix version: {}\", nix_version);\n}\n\n#[tokio::test]\nasync fn test_parse_nix_version() {\n    assert_eq!(\n        NixVersion::from_str(\"nix (Nix) 2.13.0\"),\n        Ok(NixVersion {\n            major: 2,\n            minor: 13,\n            patch: 0\n        })\n    );\n\n    // Parse simple nix version\n    assert_eq!(\n        NixVersion::from_str(\"2.13.0\"),\n        Ok(NixVersion {\n            major: 2,\n            minor: 13,\n            patch: 0\n        })\n    );\n\n    // Parse Determinate Nix Version\n    assert_eq!(\n        NixVersion::from_str(\"nix (Determinate Nix 3.6.6) 2.29.0\"),\n        Ok(NixVersion {\n            major: 2,\n            minor: 29,\n            patch: 0\n        })\n    );\n}\n"
  },
  {
    "path": "crates/nix_rs/src/version_spec.rs",
    "content": "//! Version requirement spec for [NixVersion]\n\nuse std::{fmt, str::FromStr};\n\nuse regex::Regex;\nuse serde::{Deserialize, Serialize};\nuse serde_with::{DeserializeFromStr, SerializeDisplay};\nuse thiserror::Error;\n\nuse crate::version::NixVersion;\n\n/// An individual component of [NixVersionReq]\n#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\npub enum NixVersionSpec {\n    /// Version must be greater than the specified version\n    Gt(NixVersion),\n    /// Version must be greater than or equal to the specified version\n    Gteq(NixVersion),\n    /// Version must be less than the specified version\n    Lt(NixVersion),\n    /// Version must be less than or equal to the specified version\n    Lteq(NixVersion),\n    /// Version must not equal the specified version\n    Neq(NixVersion),\n}\n\n/// Version requirement for [NixVersion]\n///\n/// Example:\n/// \">=2.8, <2.14, 12.13.4\"\n#[derive(Debug, Clone, PartialEq, SerializeDisplay, DeserializeFromStr)]\npub struct NixVersionReq {\n    /// List of version specifications\n    pub specs: Vec<NixVersionSpec>,\n}\n\n/// Errors that can occur while parsing or validating version specifications\n#[derive(Error, Debug)]\npub enum BadNixVersionSpec {\n    /// Regex error\n    #[error(\"Regex error: {0}\")]\n    Regex(#[from] regex::Error),\n\n    /// Invalid [NixVersionSpec]\n    #[error(\"Parse error(regex): Invalid version spec format\")]\n    InvalidFormat,\n\n    /// Parse error (Int)\n    #[error(\"Parse error(int): Invalid version spec format\")]\n    Parse(#[from] std::num::ParseIntError),\n\n    /// An unknown comparison operator was used\n    #[error(\"Unknown operator in the Nix version spec: {0}\")]\n    UnknownOperator(String),\n}\n\nimpl FromStr for NixVersionReq {\n    type Err = BadNixVersionSpec;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        let specs = s\n            .split(',')\n            .map(str::trim)\n            .map(NixVersionSpec::from_str)\n            .collect::<Result<Vec<_>, _>>()?;\n\n        Ok(NixVersionReq { specs })\n    }\n}\n\nimpl NixVersionSpec {\n    /// Checks if a given Nix version satisfies this version specification\n    pub fn matches(&self, version: &NixVersion) -> bool {\n        match self {\n            NixVersionSpec::Gt(v) => version > v,\n            NixVersionSpec::Gteq(v) => version >= v,\n            NixVersionSpec::Lt(v) => version < v,\n            NixVersionSpec::Lteq(v) => version <= v,\n            NixVersionSpec::Neq(v) => version != v,\n        }\n    }\n}\n\nimpl FromStr for NixVersionSpec {\n    type Err = BadNixVersionSpec;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        use NixVersionSpec::{Gt, Gteq, Lt, Lteq, Neq};\n        let re = Regex::new(\n            r#\"(?x)\n            ^\n            (?P<op>>=|<=|>|<|!=)\n            (?P<major>\\d+)\n            (?:\\.\n                (?P<minor>\\d+)\n            )?\n            (?:\\.\n                (?P<patch>\\d+)\n            )?\n            $\n            \"#,\n        )?;\n\n        let captures = re.captures(s).ok_or(BadNixVersionSpec::InvalidFormat)?;\n\n        let op = captures\n            .name(\"op\")\n            .ok_or(BadNixVersionSpec::InvalidFormat)?\n            .as_str();\n        let major: u32 = captures\n            .name(\"major\")\n            .ok_or(BadNixVersionSpec::InvalidFormat)?\n            .as_str()\n            .parse()?;\n        let minor = captures\n            .name(\"minor\")\n            .map_or(Ok(0), |m| m.as_str().parse::<u32>())?;\n        let patch = captures\n            .name(\"patch\")\n            .map_or(Ok(0), |m| m.as_str().parse::<u32>())?;\n\n        let nix_version = NixVersion {\n            major,\n            minor,\n            patch,\n        };\n\n        match op {\n            \">=\" => Ok(Gteq(nix_version)),\n            \"<=\" => Ok(Lteq(nix_version)),\n            \">\" => Ok(Gt(nix_version)),\n            \"<\" => Ok(Lt(nix_version)),\n            \"!=\" => Ok(Neq(nix_version)),\n            unknown_op => Err(BadNixVersionSpec::UnknownOperator(unknown_op.to_string())),\n        }\n    }\n}\n\nimpl fmt::Display for NixVersionSpec {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            NixVersionSpec::Gt(v) => write!(f, \">{}\", v),\n            NixVersionSpec::Gteq(v) => write!(f, \">={}\", v),\n            NixVersionSpec::Lt(v) => write!(f, \"<{}\", v),\n            NixVersionSpec::Lteq(v) => write!(f, \"<={}\", v),\n            NixVersionSpec::Neq(v) => write!(f, \"!={}\", v),\n        }\n    }\n}\n\nimpl fmt::Display for NixVersionReq {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(\n            f,\n            \"{}\",\n            self.specs\n                .iter()\n                .map(|s| s.to_string())\n                .collect::<Vec<_>>()\n                .join(\", \")\n        )\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_parse() {\n        assert_eq!(\n            NixVersionSpec::from_str(\">2.8\").unwrap(),\n            NixVersionSpec::Gt(NixVersion {\n                major: 2,\n                minor: 8,\n                patch: 0\n            })\n        );\n        assert_eq!(\n            NixVersionSpec::from_str(\">2\").unwrap(),\n            NixVersionSpec::Gt(NixVersion {\n                major: 2,\n                minor: 0,\n                patch: 0\n            })\n        );\n    }\n\n    #[test]\n    fn test_matches() {\n        let req = NixVersionReq::from_str(\"!=2.9, >2.8\").unwrap();\n\n        let version = NixVersion::from_str(\"2.9.0\").unwrap();\n        assert!(!req.specs.iter().all(|spec| spec.matches(&version)));\n        let version = NixVersion::from_str(\"2.9.1\").unwrap();\n        assert!(req.specs.iter().all(|spec| spec.matches(&version)));\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/CHANGELOG.md",
    "content": "## Unreleased\n\n- Crate renamed to `omnix-ci`\n- New\n  - Introduced notion of 'steps'. Renamed 'build' to 'run'.\n    - Added a step to run `nix flake check`\n    - Support for custom steps\n- `config.rs`: Refactored to change API.\n- Locally cache `github:nix-systems` (to avoid Github API rate limit)\n- The default subflake now uses `ROOT` instead `<root>` as the key.\n\n## 1.1.0\n\n- Remove executable\n  - `nixci` executable will no longer be updated past 1.0.0; use `omnix` (`om ci`) instead.\n- Port to newer `nix_rs`\n- Use `om.ci` as configuration key\n- tests: Removed, and moved to omnix-cli crate.\n- Fix:\n  - Passing `.#foo` where \"foo\" is missing now errors out, instead of silently defaulting.\n\n## [1.0.0](https://github.com/srid/nixci/compare/0.5.0...1.0.0) (2024-07-23)\n\n### Features\n\n* add shell completions (#87)\n([1b2caf3](https://github.com/srid/nixci/commit/1b2caf369c739382e2f1c22bfb32096f65addfba)),\ncloses [#87](https://github.com/srid/nixci/issues/87)\n* **build:** Check for minimum nix version before running nixci (#75)\n([ac5a011](https://github.com/srid/nixci/commit/ac5a011c76e9537426e0265b20e46f8efea44d40)),\ncloses [#75](https://github.com/srid/nixci/issues/75)\n* **cli:** Allow `--override-input` to refer to flake name without `flake/`\nprefix of devour_flake (#74)\n([c17f42f](https://github.com/srid/nixci/commit/c17f42f3480b4b265bac0d94e7169ca01201fb9d)),\ncloses [#74](https://github.com/srid/nixci/issues/74)\n\n### Fixes\n\n* `--print-all-dependencies` should ignore `unknown-deriver` (#76)\n([d26bab1](https://github.com/srid/nixci/commit/d26bab116f19ac248a7073de9de3ae8a3ac0271f)),\ncloses [#76](https://github.com/srid/nixci/issues/76)\n* `--print-all-dependencies` should handle `unknown-deriver` (#70)\n([16815b6](https://github.com/srid/nixci/commit/16815b6c9e476defd993368d0957335f86f9c055)),\ncloses [#70](https://github.com/srid/nixci/issues/70)\n\n## [0.5.0](https://github.com/srid/nixci/compare/0.4.0...0.5.0) (2024-06-15)\n\n### Features\n\n* Avoid fetching for known `--system` combinations\n([6164d6c](https://github.com/srid/nixci/commit/6164d6c6d37ccab02ddc4943962fd7c21828054c))\n* **api:** Pass `NixCmd` explicitly around\n([6a672e2](https://github.com/srid/nixci/commit/6a672e28811f716a8cff5108dc720269d897d246))\n* Accept global options to pass to Nix\n([cca8b98](https://github.com/srid/nixci/commit/cca8b988e24d5d4e7d76e6d2398a0f2e0b686abf))\n* **cli:** add `--print-all-depedencies` to `nixci build` subcommand (#60)\n([4109ce9](https://github.com/srid/nixci/commit/4109ce9982ad2f54e769c302ab044f16f8bd865c)),\ncloses [#60](https://github.com/srid/nixci/issues/60)\n\n## 0.4.0 (Apr 19, 2024)\n\n- New features\n    - Add new config `nixci.*.*.systems` acting as a whitelist of systems to build that subflake.\n    - Add `nixci build --systems` option to build on an arbitrary systems (\\#39)\n    - Allow selecting sub-flake to build, e.g.: `nixci .#default.myflake`  (\\#45)\n    - Add subcommand to generate Github Actions matrix (\\#50)\n        - Consequently, you must run `nixci build` instead of `nixci` now.\n    - Pass `--extra-experimental-features` only when necessary. Simplifies logging. (#46)\n- Fixes\n    - Fix regression in Nix 2.19+ (`devour-flake produced an outpath with no outputs`) (\\#35)\n    - Evaluate OS configurations for current system only (\\#38)\n    - Fail correctly if nixci is passed a missing flake attribute (\\#44)\n\n## 0.2.0 (Sep 14, 2023)\n\n- Breaking changes\n    - Change flake schema: evaluates `nixci.default` instead of `nixci`; this allows more than one configuration (#20)\n- Pass the rest of CLI arguments after `--` as-is to `nix build`\n    - Consequently, remove `--rebuild`, `--no-refresh` and `--system` options, because these can be specified using the new CLI spec.\n- Bug fixes\n    - Fix nixci breaking if branch name of a PR has `#` (#17)\n- Misc changes\n    - Iterate configs in a deterministic order\n    - stdout outputs are uniquely printed, in sorted order\n    - stderr output is now logged using the `tracing` crate.\n    - Pass `--extra-experimental-features` to enable flakes\n    - `nixci` can now be used as a Rust library\n    - `nixci` no longer depends on `devour-flake` the *executable package*, only on the flake.\n\n## 0.1.3\n\n- Pass `-j auto` to nix builds.\n\n## 0.1.2\n\nInitial release\n"
  },
  {
    "path": "crates/omnix-ci/Cargo.toml",
    "content": "[package]\nauthors = [\"Sridhar Ratnakumar <srid@srid.ca>\"]\nedition = \"2021\"\n# If you change the name here, you must also do it in flake.nix (and run `cargo generate-lockfile` afterwards)\nname = \"omnix-ci\"\nversion = \"1.3.2\"\nlicense = \"AGPL-3.0-only\"\nreadme = \"README.md\"\ndescription = \"Define and build CI for Nix projects anywhere\"\nhomepage = \"https://omnix.page\"\nrepository = \"https://github.com/juspay/omnix\"\nkeywords = [\"nix\"]\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nanyhow = { workspace = true }\nclap = { workspace = true }\ncolored = { workspace = true }\nfutures-lite = { workspace = true }\nlazy_static = { workspace = true }\nomnix-health = { workspace = true }\nnix_rs = { workspace = true, features = [\"clap\"] }\nnonempty = { workspace = true }\nomnix-common = { workspace = true }\nreqwest = { workspace = true }\nserde = { workspace = true }\nserde_json = { workspace = true }\nshell-words = { workspace = true }\ntempfile = { workspace = true }\nthiserror = { workspace = true }\ntokio = { workspace = true }\ntracing = { workspace = true }\ntracing-subscriber = { workspace = true }\ntry-guard = { workspace = true }\nurl = { workspace = true }\nurlencoding = { workspace = true }\n"
  },
  {
    "path": "crates/omnix-ci/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "crates/omnix-ci/README.md",
    "content": "# `omnix-ci`\n\n[![Crates.io](https://img.shields.io/crates/v/omnix-ci.svg)](https://crates.io/crates/omnix-ci)\n\nThe Rust crate responsible for [`om ci`](https://omnix.page/om/ci.html).\n"
  },
  {
    "path": "crates/omnix-ci/crate.nix",
    "content": "{ pkgs\n, lib\n, ...\n}:\n{\n  autoWire = [ ];\n  crane = {\n    args = {\n      nativeBuildInputs = with pkgs; lib.optionals stdenv.isDarwin [\n        libiconv\n        pkg-config\n      ];\n      buildInputs = lib.optionals pkgs.stdenv.isLinux [\n        pkgs.openssl\n      ];\n      # Disable tests due to sandboxing issues; we run them on CI\n      # instead.\n      doCheck = false;\n    };\n  };\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/command/core.rs",
    "content": "//! The `om ci` subcommands\nuse clap::Subcommand;\nuse colored::Colorize;\nuse nix_rs::command::NixCmd;\nuse omnix_common::config::OmConfig;\nuse tracing::instrument;\n\nuse crate::flake_ref::FlakeRef;\n\nuse super::{gh_matrix::GHMatrixCommand, run::RunCommand};\n\n/// Top-level commands for `om ci`\n#[derive(Debug, Subcommand, Clone)]\npub enum Command {\n    /// Run all CI steps for all or given subflakes\n    Run(RunCommand),\n\n    /// Print the Github Actions matrix configuration as JSON\n    #[clap(name = \"gh-matrix\")]\n    DumpGithubActionsMatrix(GHMatrixCommand),\n}\n\nimpl Default for Command {\n    fn default() -> Self {\n        Self::Run(Default::default())\n    }\n}\n\nimpl Command {\n    /// Run the command\n    #[instrument(name = \"run\", skip(self))]\n    pub async fn run(self) -> anyhow::Result<()> {\n        tracing::info!(\"{}\", \"\\n👟 Reading om.ci config from flake\".bold());\n        let url = self.get_flake_ref().to_flake_url().await?;\n        let cfg = OmConfig::get(self.nixcmd(), &url).await?;\n\n        tracing::debug!(\"OmConfig: {cfg:?}\");\n        match self {\n            Command::Run(cmd) => cmd.run(cfg).await,\n            Command::DumpGithubActionsMatrix(cmd) => cmd.run(cfg).await,\n        }\n    }\n\n    fn nixcmd(&self) -> &NixCmd {\n        match self {\n            Command::Run(cmd) => &cmd.nixcmd,\n            Command::DumpGithubActionsMatrix(cmd) => &cmd.nixcmd,\n        }\n    }\n\n    /// Get the [FlakeRef] associated with this subcommand\n    fn get_flake_ref(&self) -> &FlakeRef {\n        match self {\n            Command::Run(cmd) => &cmd.flake_ref,\n            Command::DumpGithubActionsMatrix(cmd) => &cmd.flake_ref,\n        }\n    }\n\n    /// Convert this type back to the user-facing command line arguments\n    pub fn to_cli_args(&self) -> Vec<String> {\n        let mut args = vec![\"ci\".to_string(), \"run\".to_string()];\n        match self {\n            Command::Run(cmd) => {\n                args.extend(cmd.to_cli_args());\n            }\n            Command::DumpGithubActionsMatrix(_cmd) => {\n                unimplemented!(\"Command::DumpGithubActionsMatrix::to_cli_args\")\n            }\n        }\n        args\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/command/gh_matrix.rs",
    "content": "//! The gh-matrix command\nuse clap::Parser;\nuse nix_rs::{command::NixCmd, flake::system::System};\nuse omnix_common::config::OmConfig;\n\nuse crate::{config::subflakes::SubflakesConfig, flake_ref::FlakeRef, github};\n\n/// Command to generate a Github Actions matrix\n#[derive(Parser, Debug, Clone)]\npub struct GHMatrixCommand {\n    /// Flake URL or github URL\n    ///\n    /// A specific omnix-ci configuration can be specified\n    /// using '#': e.g. `om ci run .#extra-tests`\n    #[arg(default_value = \".\")]\n    pub flake_ref: FlakeRef,\n\n    /// Systems to include in the matrix\n    #[arg(long, value_parser, value_delimiter = ',')]\n    pub systems: Vec<System>,\n\n    /// Nix command global options\n    #[command(flatten)]\n    pub nixcmd: NixCmd,\n}\n\nimpl GHMatrixCommand {\n    /// Run the command\n    pub async fn run(&self, cfg: OmConfig) -> anyhow::Result<()> {\n        let (config, _rest) = cfg.get_sub_config_under::<SubflakesConfig>(\"ci\")?;\n        let matrix = github::matrix::GitHubMatrix::from(self.systems.clone(), &config);\n        println!(\"{}\", serde_json::to_string(&matrix)?);\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/command/mod.rs",
    "content": "//! CLI commands for omnix-ci\npub mod core;\npub mod gh_matrix;\npub mod run;\npub mod run_remote;\n"
  },
  {
    "path": "crates/omnix-ci/src/command/run.rs",
    "content": "//! The run command\nuse std::{\n    collections::HashMap,\n    env,\n    io::Write,\n    path::{Path, PathBuf},\n};\n\nuse anyhow::{Context, Result};\nuse clap::Parser;\nuse colored::Colorize;\nuse nix_rs::{\n    command::NixCmd,\n    config::NixConfig,\n    flake::{functions::addstringcontext, system::System, url::FlakeUrl},\n    info::NixInfo,\n    store::{path::StorePath, uri::StoreURI},\n    system_list::{SystemsList, SystemsListFlakeRef},\n};\nuse omnix_common::config::OmConfig;\nuse omnix_health::{traits::Checkable, NixHealth};\nuse serde::{Deserialize, Serialize};\n\nuse crate::{\n    config::subflakes::SubflakesConfig, flake_ref::FlakeRef, github::actions::in_github_log_group,\n    step::core::StepsResult,\n};\n\nuse super::run_remote;\n\n/// Run all CI steps for all or given subflakes\n/// Command to run all CI steps\n#[derive(Parser, Debug, Clone)]\npub struct RunCommand {\n    /// Run `om ci run` remotely on the given store URI\n    #[clap(long)]\n    pub on: Option<StoreURI>,\n\n    /// The systems list to build for. If empty, build for current system.\n    ///\n    /// Must be a flake reference which, when imported, must return a Nix list\n    /// of systems. You may use one of the lists from\n    /// <https://github.com/nix-systems>.\n    ///\n    /// You can also pass the individual system name, if they are supported by omnix.\n    #[arg(long)]\n    pub systems: Option<SystemsListFlakeRef>,\n\n    /// Symlink to build results (as JSON)\n    #[arg(\n        long,\n        short = 'o',\n        default_value = \"result\",\n        conflicts_with = \"no_link\",\n        alias = \"results\", // For backwards compat\n        name = \"PATH\"\n    )]\n    out_link: Option<PathBuf>,\n\n    /// Do not create a symlink to build results JSON\n    #[arg(long)]\n    no_link: bool,\n\n    /// Flake URL or github URL\n    ///\n    /// A specific configuration can be specified\n    /// using '#': e.g. `om ci run .#default.extra-tests`\n    #[arg(default_value = \".\")]\n    pub flake_ref: FlakeRef,\n\n    /// Print Github Actions log groups (enabled by default when run in Github Actions)\n    #[clap(long, default_value_t = env::var(\"GITHUB_ACTION\").is_ok())]\n    pub github_output: bool,\n\n    /// Arguments for all steps\n    #[command(flatten)]\n    pub steps_args: crate::step::core::StepsArgs,\n\n    /// Nix command global options\n    #[command(flatten)]\n    pub nixcmd: NixCmd,\n}\n\nimpl Default for RunCommand {\n    fn default() -> Self {\n        RunCommand::parse_from::<[_; 0], &str>([])\n    }\n}\n\nimpl RunCommand {\n    /// Get the out-link path\n    pub fn get_out_link(&self) -> Option<&Path> {\n        if self.no_link {\n            None\n        } else {\n            self.out_link.as_ref().map(PathBuf::as_ref)\n        }\n    }\n\n    /// Override the `flake_ref` and `out_link`` for building locally.\n    pub fn local_with(&self, flake_ref: FlakeRef, out_link: Option<PathBuf>) -> Self {\n        let mut new = self.clone();\n        new.on = None; // Disable remote building\n        new.flake_ref = flake_ref;\n        new.no_link = out_link.is_none();\n        new.out_link = out_link;\n        new\n    }\n\n    /// Run the build command which decides whether to do ci run on current machine or a remote machine\n    pub async fn run(&self, cfg: OmConfig) -> anyhow::Result<()> {\n        match &self.on {\n            Some(store_uri) => {\n                run_remote::run_on_remote_store(&self.nixcmd, self, &cfg, store_uri).await\n            }\n            None => self.run_local(cfg).await,\n        }\n    }\n\n    /// Run [RunCommand] on local Nix store.\n    async fn run_local(&self, cfg: OmConfig) -> anyhow::Result<()> {\n        // TODO: We'll refactor this function to use steps\n        // https://github.com/juspay/omnix/issues/216\n\n        let nix_info = in_github_log_group(\"info\", self.github_output, || async {\n            tracing::info!(\"{}\", \"\\n👟 Gathering NixInfo\".bold());\n            NixInfo::get()\n                .await\n                .as_ref()\n                .with_context(|| \"Unable to gather nix info\")\n        })\n        .await?;\n\n        // First, run the necessary health checks\n        in_github_log_group(\"health\", self.github_output, || async {\n            tracing::info!(\"{}\", \"\\n🫀 Performing health check\".bold());\n            // check_nix_version(&cfg, nix_info).await?;\n            check_nix_version(&cfg, nix_info).await\n        })\n        .await?;\n\n        // Then, do the CI steps\n        tracing::info!(\n            \"{}\",\n            format!(\"\\n🤖 Running CI for {}\", self.flake_ref).bold()\n        );\n        let res = ci_run(&self.nixcmd, self, &cfg, &nix_info.nix_config).await?;\n\n        let msg = in_github_log_group::<anyhow::Result<String>, _, _>(\n            \"outlink\",\n            self.github_output,\n            || async {\n                let m_out_link = self.get_out_link();\n                let s = serde_json::to_string(&res)?;\n                let mut path = tempfile::Builder::new()\n                    .prefix(\"om-ci-results-\")\n                    .suffix(\".json\")\n                    .tempfile()?;\n                path.write_all(s.as_bytes())?;\n\n                let results_path =\n                    addstringcontext::addstringcontext(&self.nixcmd, path.path(), m_out_link)\n                        .await?;\n                println!(\"{}\", results_path.display());\n\n                let msg = format!(\n                    \"Result available at {:?}{}\",\n                    results_path.as_path(),\n                    m_out_link\n                        .map(|p| format!(\" and symlinked at {:?}\", p))\n                        .unwrap_or_default()\n                );\n                Ok(msg)\n            },\n        )\n        .await?;\n\n        tracing::info!(\"{}\", msg);\n\n        Ok(())\n    }\n\n    /// Get the systems to build for\n    pub async fn get_systems(&self, cmd: &NixCmd, nix_config: &NixConfig) -> Result<Vec<System>> {\n        match &self.systems {\n            None => {\n                // An empty systems list means build for the current system\n                let current_system = &nix_config.system.value;\n                Ok(vec![current_system.clone()])\n            }\n            Some(systems) => {\n                let systems = SystemsList::from_flake(cmd, systems).await?.0;\n                Ok(systems)\n            }\n        }\n    }\n\n    /// Convert this type back to the user-facing command line arguments\n    pub fn to_cli_args(&self) -> Vec<String> {\n        let mut args = vec![];\n\n        if let Some(uri) = self.on.as_ref() {\n            args.push(\"--on\".to_owned());\n            args.push(uri.to_string());\n        }\n\n        if let Some(systems) = self.systems.as_ref() {\n            args.push(\"--systems\".to_string());\n            args.push(systems.0 .0.clone());\n        }\n\n        if let Some(out_link) = self.out_link.as_ref() {\n            args.push(\"--out-link\".to_string());\n            args.push(out_link.to_string_lossy().to_string());\n        }\n\n        if self.no_link {\n            args.push(\"--no-link\".to_string());\n        }\n\n        args.push(self.flake_ref.to_string());\n\n        args.extend(self.steps_args.to_cli_args());\n\n        args\n    }\n}\n\n/// Check that Nix version is not too old.\npub async fn check_nix_version(cfg: &OmConfig, nix_info: &NixInfo) -> anyhow::Result<()> {\n    let omnix_health = NixHealth::from_om_config(cfg)?;\n    let checks = omnix_health\n        .nix_version\n        .check(nix_info, Some(&cfg.flake_url));\n    let exit_code = NixHealth::print_report_returning_exit_code(&checks, false).await?;\n\n    if exit_code != 0 {\n        std::process::exit(exit_code);\n    }\n    Ok(())\n}\n\n/// Run CI for all subflakes\npub async fn ci_run(\n    cmd: &NixCmd,\n    run_cmd: &RunCommand,\n    cfg: &OmConfig,\n    nix_config: &NixConfig,\n) -> anyhow::Result<RunResult> {\n    let mut res = HashMap::new();\n    let systems = run_cmd.get_systems(cmd, nix_config).await?;\n\n    let (config, attrs) = cfg.get_sub_config_under::<SubflakesConfig>(\"ci\")?;\n\n    // User's filter by subflake name\n    let only_subflake = attrs.first();\n\n    for (subflake_name, subflake) in &config.0 {\n        let name = subflake_name.italic();\n\n        if let Some(s) = only_subflake {\n            if s != subflake_name {\n                tracing::info!(\"\\n🍊 {} {}\", name, \"skipped (deselected out)\".dimmed());\n                continue;\n            }\n        }\n\n        let compatible_system = subflake.can_run_on(&systems);\n        if !compatible_system {\n            tracing::info!(\n                \"\\n🍊 {} {}\",\n                name,\n                \"skipped (cannot run on this system)\".dimmed()\n            );\n            continue;\n        }\n\n        let steps_res = in_github_log_group(\n            &format!(\"subflake={}\", name),\n            run_cmd.github_output,\n            || async {\n                tracing::info!(\"\\n🍎 {}\", name);\n                subflake\n                    .steps\n                    .run(cmd, run_cmd, &systems, &cfg.flake_url, subflake)\n                    .await\n            },\n        )\n        .await?;\n        res.insert(subflake_name.clone(), steps_res);\n    }\n\n    tracing::info!(\"\\n🥳 Success!\");\n\n    Ok(RunResult {\n        systems,\n        flake: cfg.flake_url.clone(),\n        result: res,\n    })\n}\n\n/// Results of the 'ci run' command\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct RunResult {\n    /// The systems we are building for\n    systems: Vec<System>,\n    /// The flake being built\n    flake: FlakeUrl,\n    /// CI result for each subflake\n    result: HashMap<String, StepsResult>,\n}\n\nimpl RunResult {\n    /// Get all store paths mentioned in this type.\n    pub fn all_out_paths(&self) -> Vec<StorePath> {\n        let mut res = vec![];\n        for steps_res in self.result.values() {\n            if let Some(build) = steps_res.build_step.as_ref() {\n                res.extend(build.devour_flake_output.out_paths.clone());\n            }\n        }\n        res\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/command/run_remote.rs",
    "content": "//! Functions for running `ci run` on remote machine.\n\nuse colored::Colorize;\nuse nix_rs::{\n    command::{CommandError, NixCmd},\n    copy::{nix_copy, NixCopyOptions},\n    flake::{\n        functions::metadata::{FlakeMetadata, FlakeMetadataInput},\n        url::FlakeUrl,\n    },\n    store::{command::NixStoreCmd, path::StorePath, uri::StoreURI},\n};\nuse omnix_common::config::OmConfig;\nuse std::{\n    ffi::{OsStr, OsString},\n    os::unix::ffi::OsStringExt,\n    path::{Path, PathBuf},\n};\nuse tokio::process::Command;\n\nuse super::run::RunCommand;\n\n/// Path to Rust source corresponding to this (running) instance of Omnix\nconst OMNIX_SOURCE: &str = env!(\"OMNIX_SOURCE\");\n\n/// Like [RunCommand::run] but run on a remote Nix store.\npub async fn run_on_remote_store(\n    nixcmd: &NixCmd,\n    run_cmd: &RunCommand,\n    cfg: &OmConfig,\n    store_uri: &StoreURI,\n) -> anyhow::Result<()> {\n    let StoreURI::SSH(ssh_uri, opts) = store_uri;\n    tracing::info!(\n        \"{}\",\n        format!(\"\\n🛜 Running CI remotely on {} ({:?})\", ssh_uri, opts).bold()\n    );\n\n    let (flake_closure, local_flake_url) = &cache_flake(nixcmd, cfg, opts.copy_inputs).await?;\n    let omnix_source = PathBuf::from(OMNIX_SOURCE);\n\n    let paths_to_push = vec![omnix_source, flake_closure.clone()];\n\n    // First, copy the flake and omnix source to the remote store, because we will be needing them when running over ssh.\n    nix_copy_to_remote(nixcmd, store_uri, &paths_to_push).await?;\n\n    // If out-link is requested, we need to copy the results back to local store - so that when we create the out-link *locally* the paths in it refer to valid paths in the local store. Thus, --out-link can be used to trick Omnix into copying all built paths back.\n    if let Some(out_link) = run_cmd.get_out_link() {\n        // A temporary location on ssh remote to hold the result\n        let tmpdir = parse_path_line(\n            &run_ssh_with_output(\n                &ssh_uri.to_string(),\n                &nixpkgs_cmd(\"coreutils\", &[\"mktemp\", \"-d\", \"-t\", \"om.json.XXXXXX\"]),\n            )\n            .await?,\n        );\n        let om_json_path = tmpdir.join(\"om.json\");\n\n        // Then, SSH and run the same `om ci run` CLI but without the `--on` argument but with `--out-link` pointing to the temporary location.\n        run_ssh(\n            &ssh_uri.to_string(),\n            &om_cli_with(\n                run_cmd.local_with(local_flake_url.clone().into(), Some(om_json_path.clone())),\n            ),\n        )\n        .await?;\n\n        // Get the out-link store path.\n        let om_result_path: StorePath = StorePath::new(parse_path_line(\n            &run_ssh_with_output(\n                &ssh_uri.to_string(),\n                &nixpkgs_cmd(\n                    \"coreutils\",\n                    &[\"readlink\", om_json_path.to_string_lossy().as_ref()],\n                ),\n            )\n            .await?,\n        ));\n\n        // Copy the results back to local store (the out-link).\n        tracing::info!(\"{}\", \"📦 Copying results back to local store\".bold());\n        nix_copy_from_remote(nixcmd, store_uri, &[&om_result_path]).await?;\n\n        // Write the local out-link\n        let nix_store = NixStoreCmd {};\n        nix_store\n            .nix_store_add_root(out_link, &[&om_result_path])\n            .await?;\n        tracing::info!(\n            \"Results available at {:?} symlinked at {:?}\",\n            om_result_path.as_path(),\n            out_link\n        );\n    } else {\n        // Then, SSH and run the same `om ci run` CLI but without the `--on` argument.\n        run_ssh(\n            &ssh_uri.to_string(),\n            &om_cli_with(run_cmd.local_with(local_flake_url.clone().into(), None)),\n        )\n        .await?;\n    }\n    Ok(())\n}\n\nasync fn nix_copy_to_remote<I, P>(\n    nixcmd: &NixCmd,\n    store_uri: &StoreURI,\n    paths: I,\n) -> Result<(), CommandError>\nwhere\n    I: IntoIterator<Item = P>,\n    P: AsRef<Path> + AsRef<OsStr>,\n{\n    nix_copy(\n        nixcmd,\n        NixCopyOptions {\n            to: Some(store_uri.to_owned()),\n            no_check_sigs: true,\n            ..Default::default()\n        },\n        paths,\n    )\n    .await\n}\n\nasync fn nix_copy_from_remote<I, P>(\n    nixcmd: &NixCmd,\n    store_uri: &StoreURI,\n    paths: I,\n) -> Result<(), CommandError>\nwhere\n    I: IntoIterator<Item = P>,\n    P: AsRef<Path> + AsRef<OsStr>,\n{\n    nix_copy(\n        nixcmd,\n        NixCopyOptions {\n            from: Some(store_uri.to_owned()),\n            no_check_sigs: true,\n            ..Default::default()\n        },\n        paths,\n    )\n    .await\n}\n\nfn parse_path_line(bytes: &[u8]) -> PathBuf {\n    let trimmed_bytes = bytes.trim_ascii_end();\n    PathBuf::from(OsString::from_vec(trimmed_bytes.to_vec()))\n}\n\n/// Construct CLI arguments for running a program from nixpkgs using given arguments\nfn nixpkgs_cmd(package: &str, cmd: &[&str]) -> Vec<String> {\n    let mut args = vec![\n        \"nix\".to_owned(),\n        \"shell\".to_owned(),\n        format!(\"nixpkgs#{}\", package),\n    ];\n    args.push(\"-c\".to_owned());\n    args.extend(cmd.iter().map(|s| s.to_string()));\n    args\n}\n\n/// Return the locally cached [FlakeUrl] for the given flake url that points to same selected [ConfigRef].\nasync fn cache_flake(\n    nixcmd: &NixCmd,\n    cfg: &OmConfig,\n    include_inputs: bool,\n) -> anyhow::Result<(PathBuf, FlakeUrl)> {\n    let (closure, metadata) = FlakeMetadata::from_nix(\n        nixcmd,\n        FlakeMetadataInput {\n            flake: cfg.flake_url.clone(),\n            include_inputs,\n        },\n    )\n    .await?;\n    let attr = cfg.reference.join(\".\");\n    let mut local_flake_url = Into::<FlakeUrl>::into(metadata.flake.clone());\n    if !attr.is_empty() {\n        local_flake_url = local_flake_url.with_attr(&attr);\n    }\n    Ok((closure, local_flake_url))\n}\n\n/// Construct a `nix run ...` based CLI that runs Omnix using given arguments.\n///\n/// Omnix itself will be compiled from source ([OMNIX_SOURCE]) if necessary. Thus, this invocation is totally independent and can be run on remote machines, as long as the paths exista on the nix store.\nfn om_cli_with(run_cmd: RunCommand) -> Vec<String> {\n    let mut args: Vec<String> = vec![];\n\n    let omnix_flake = format!(\"{}#default\", OMNIX_SOURCE);\n    args.extend(\n        [\n            \"nix\",\n            \"--accept-flake-config\",\n            \"run\",\n            &omnix_flake,\n            \"--\",\n            \"ci\",\n            \"run\",\n        ]\n        .map(&str::to_owned),\n    );\n    args.extend(run_cmd.to_cli_args());\n    args\n}\n\n/// Run SSH command with given arguments.\nasync fn run_ssh(host: &str, args: &[String]) -> anyhow::Result<()> {\n    let mut cmd = Command::new(\"ssh\");\n    cmd.args([host, &shell_words::join(args)]);\n\n    nix_rs::command::trace_cmd_with(\"🐌\", &cmd);\n\n    let status = cmd.status().await?;\n    if !status.success() {\n        return Err(anyhow::anyhow!(\"SSH command failed: {}\", status));\n    }\n    Ok(())\n}\n\n/// Run SSH command with given arguments and return the stdout.\nasync fn run_ssh_with_output<I, S>(host: &str, args: I) -> anyhow::Result<Vec<u8>>\nwhere\n    I: IntoIterator<Item = S>,\n    S: AsRef<str>,\n{\n    let mut cmd = Command::new(\"ssh\");\n    cmd.args([host, &shell_words::join(args)]);\n\n    nix_rs::command::trace_cmd_with(\"🐌\", &cmd);\n\n    let output = cmd.output().await?;\n    if output.status.success() {\n        Ok(output.stdout)\n    } else {\n        Err(anyhow::anyhow!(\n            \"SSH command failed: {}\",\n            String::from_utf8_lossy(&output.stderr)\n        ))\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/config/core.rs",
    "content": "//! The top-level configuration of omnix-ci, as defined in flake.nix\n\n#[cfg(test)]\nmod tests {\n    use nix_rs::{command::NixCmd, flake::url::FlakeUrl};\n    use omnix_common::config::OmConfig;\n\n    use crate::config::subflakes::SubflakesConfig;\n\n    #[tokio::test]\n    async fn test_config_loading() {\n        // Testing this flake:\n        // https://github.com/srid/haskell-flake/blob/c60351652c71ebeb5dd237f7da874412a7a96970/flake.nix#L30-L95\n        let url = &FlakeUrl(\n            \"github:srid/haskell-flake/c60351652c71ebeb5dd237f7da874412a7a96970#default.dev\"\n                .to_string(),\n        );\n        let cfg = OmConfig::get(NixCmd::get().await, url).await.unwrap();\n        let (config, attrs) = cfg.get_sub_config_under::<SubflakesConfig>(\"ci\").unwrap();\n        assert_eq!(attrs, &[\"dev\"]);\n        // assert_eq!(cfg.selected_subconfig, Some(\"dev\".to_string()));\n        assert_eq!(config.0.len(), 9);\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/config/mod.rs",
    "content": "//! omnix-ci config defined in `flake.nix`\npub mod core;\npub mod subflake;\npub mod subflakes;\n"
  },
  {
    "path": "crates/omnix-ci/src/config/subflake.rs",
    "content": "//! Subflake configuration\nuse std::collections::BTreeMap;\n\nuse nix_rs::flake::{system::System, url::FlakeUrl};\nuse serde::Deserialize;\n\nuse crate::step::core::Steps;\n\n/// Represents a sub-flake look-alike.\n///\n/// \"Look-alike\" because its inputs may be partial, thus requiring explicit\n/// --override-inputs when evaluating the flake.\n#[derive(Debug, Deserialize, Clone)]\npub struct SubflakeConfig {\n    /// Whether to skip building this subflake\n    #[serde(default)]\n    pub skip: bool,\n\n    /// Subdirectory in which the flake lives\n    pub dir: String,\n\n    /// Inputs to override (via --override-input)\n    // NB: we use BTreeMap instead of HashMap here so that we always iterate\n    // inputs in a determinitstic (i.e. asciibetical) order\n    #[serde(rename = \"overrideInputs\", default)]\n    pub override_inputs: BTreeMap<String, FlakeUrl>,\n\n    /// An optional whitelist of systems to build on (others are ignored)\n    pub systems: Option<Vec<System>>,\n\n    /// List of CI steps to run\n    #[serde(default)]\n    pub steps: Steps,\n}\n\nimpl Default for SubflakeConfig {\n    /// The default `SubflakeConfig` is the root flake.\n    fn default() -> Self {\n        SubflakeConfig {\n            skip: false,\n            dir: \".\".to_string(),\n            override_inputs: BTreeMap::default(),\n            systems: None,\n            steps: Steps::default(),\n        }\n    }\n}\n\nimpl SubflakeConfig {\n    /// Whether the CI for this subflake can be run on any of the given systems\n    pub fn can_run_on(&self, systems: &[System]) -> bool {\n        match self.systems.as_ref() {\n            Some(systems_whitelist) => systems_whitelist.iter().any(|s| systems.contains(s)),\n            None => true,\n        }\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/config/subflakes.rs",
    "content": "//! Subflakes configuration group.\nuse std::collections::BTreeMap;\n\nuse serde::Deserialize;\n\nuse super::subflake::SubflakeConfig;\n\n/// CI configuration for a subflake\n#[derive(Debug, Deserialize, Clone)]\npub struct SubflakesConfig(\n    // NB: we use BTreeMap instead of HashMap here so that we always iterate\n    // configs in a determinitstic (i.e. asciibetical) order\n    pub BTreeMap<String, SubflakeConfig>,\n);\n\nimpl Default for SubflakesConfig {\n    /// Default value contains a single entry for the root flake.\n    fn default() -> Self {\n        let mut subflakes = BTreeMap::new();\n        subflakes.insert(\"ROOT\".to_string(), SubflakeConfig::default());\n        SubflakesConfig(subflakes)\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/flake_ref.rs",
    "content": "//! A reference to some flake living somewhere\nuse std::{\n    fmt::{Display, Formatter},\n    str::FromStr,\n};\n\nuse anyhow::Result;\nuse nix_rs::flake::url::FlakeUrl;\n\nuse crate::github::pull_request::{PullRequest, PullRequestRef};\n\n/// A reference to some flake living somewhere\n///\n/// This type captures the superset of what flake URLs allow.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub enum FlakeRef {\n    /// A github PR\n    GithubPR(PullRequestRef),\n    /// A flake URL supported by Nix commands\n    Flake(FlakeUrl),\n}\n\nimpl FromStr for FlakeRef {\n    type Err = String;\n    fn from_str(s: &str) -> std::result::Result<FlakeRef, String> {\n        let flake_ref = match PullRequestRef::from_web_url(s) {\n            Some(pr) => FlakeRef::GithubPR(pr),\n            None => FlakeRef::Flake(FlakeUrl(s.to_string())),\n        };\n        Ok(flake_ref)\n    }\n}\n\nimpl From<FlakeUrl> for FlakeRef {\n    fn from(url: FlakeUrl) -> Self {\n        FlakeRef::Flake(url)\n    }\n}\n\nimpl Display for FlakeRef {\n    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {\n        match self {\n            FlakeRef::GithubPR(pr) => write!(f, \"{}\", pr),\n            FlakeRef::Flake(url) => write!(f, \"{}\", url),\n        }\n    }\n}\n\nimpl FlakeRef {\n    /// Convert the value to a flake URL that Nix command will recognize.\n    pub async fn to_flake_url(&self) -> Result<FlakeUrl> {\n        match self {\n            FlakeRef::GithubPR(pr) => {\n                let pr = PullRequest::get(pr).await?;\n                Ok(pr.flake_url())\n            }\n            FlakeRef::Flake(url) => Ok(url.clone()),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_github_pr() {\n        assert_eq!(\n            FlakeRef::from_str(\"https://github.com/srid/nixci/pull/19\").unwrap(),\n            FlakeRef::GithubPR(PullRequestRef {\n                owner: \"srid\".to_string(),\n                repo: \"nixci\".to_string(),\n                pr: 19\n            })\n        );\n    }\n\n    #[test]\n    fn test_current_dir() {\n        assert_eq!(\n            FlakeRef::from_str(\".\").unwrap(),\n            FlakeRef::Flake(FlakeUrl(\".\".to_string()))\n        );\n    }\n\n    #[test]\n    fn test_flake_url() {\n        assert_eq!(\n            FlakeRef::from_str(\"github:srid/nixci\").unwrap(),\n            FlakeRef::Flake(FlakeUrl(\"github:srid/nixci\".to_string()))\n        );\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/github/actions.rs",
    "content": "//! Working with GitHub Actions\n\nuse std::future::Future;\n\n/// Group log lines in GitHub Actions\n///\n/// https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#grouping-log-lines\npub async fn in_github_log_group<T, F, Fut>(name: &str, enable: bool, f: F) -> T\nwhere\n    F: FnOnce() -> Fut,\n    Fut: Future<Output = T>,\n{\n    if enable {\n        eprintln!(\"::group::{}\", name);\n    }\n\n    let result = f().await;\n\n    if enable {\n        eprintln!(\"::endgroup::\");\n    }\n\n    result\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/github/matrix.rs",
    "content": "//! Github Actions matrix\nuse nix_rs::flake::system::System;\nuse serde::{Deserialize, Serialize};\n\nuse crate::config::subflakes::SubflakesConfig;\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n/// A row in the Github Actions matrix configuration\npub struct GitHubMatrixRow {\n    /// System to build on\n    pub system: System,\n    /// Subflake to build\n    pub subflake: String,\n}\n\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n/// Github Actions matrix configuration\npub struct GitHubMatrix {\n    /// The includes\n    pub include: Vec<GitHubMatrixRow>,\n}\n\nimpl GitHubMatrix {\n    /// Create a [GitHubMatrix] for the given subflakes and systems\n    pub fn from(systems: Vec<System>, subflakes: &SubflakesConfig) -> Self {\n        let include: Vec<GitHubMatrixRow> = systems\n            .iter()\n            .flat_map(|system| {\n                subflakes\n                    .0\n                    .iter()\n                    .filter(|&(_k, v)| v.can_run_on(std::slice::from_ref(system)))\n                    .map(|(k, _v)| GitHubMatrixRow {\n                        system: system.clone(),\n                        subflake: k.clone(),\n                    })\n            })\n            .collect();\n        GitHubMatrix { include }\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/github/mod.rs",
    "content": "//! GitHub related types and functions.\npub mod actions;\npub mod matrix;\npub mod pull_request;\n"
  },
  {
    "path": "crates/omnix-ci/src/github/pull_request.rs",
    "content": "//! Github Pull Request API\nuse std::fmt::Display;\n\n/// Enough types to get branch info from Pull Request URL\nuse anyhow::{bail, Context};\nuse nix_rs::flake::url::FlakeUrl;\nuse reqwest::header::USER_AGENT;\nuse serde::Deserialize;\nuse try_guard::guard;\nuse url::{Host, Url};\n\n/// A reference to a Github Pull Request\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct PullRequestRef {\n    pub(crate) owner: String,\n    pub(crate) repo: String,\n    pub(crate) pr: u64,\n}\n\nimpl Display for PullRequestRef {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(\n            f,\n            \"https://github.com/{}/{}/pull/{}\",\n            self.owner, self.repo, self.pr\n        )\n    }\n}\n\nimpl PullRequestRef {\n    fn api_url(&self) -> String {\n        format!(\n            \"https://api.github.com/repos/{}/{}/pulls/{}\",\n            self.owner, self.repo, self.pr\n        )\n    }\n    /// Parse a Github PR URL into its owner, repo, and PR number\n    pub fn from_web_url(url: &str) -> Option<Self> {\n        let url = Url::parse(url).ok()?;\n        guard!(url.scheme() == \"https\" && url.host() == Some(Host::Domain(\"github.com\")));\n        let paths = url.path_segments().map(|c| c.collect::<Vec<_>>())?;\n        match paths[..] {\n            [user, repo, \"pull\", pr_] => {\n                let pr = pr_.parse::<u64>().ok()?;\n                Some(PullRequestRef {\n                    owner: user.to_string(),\n                    repo: repo.to_string(),\n                    pr,\n                })\n            }\n            _ => None,\n        }\n    }\n}\n\n/// Github Pull Request API Response\n#[derive(Debug, Deserialize)]\npub struct PullRequest {\n    /// PR URL\n    pub url: String,\n    /// [Head] info\n    pub head: Head,\n}\n\n/// Pull Request head info\n#[derive(Debug, Deserialize)]\npub struct Head {\n    #[serde(rename = \"ref\")]\n    /// Head ref\n    pub ref_: String,\n    /// Head [Repo]\n    pub repo: Repo,\n}\n\n/// Pull Request repo info\n#[derive(Debug, Deserialize)]\npub struct Repo {\n    /// `<owner>/<repo>`\n    pub full_name: String,\n}\n\nimpl PullRequest {\n    /// Fetch the given PR using Github's API\n    pub async fn get(ref_: &PullRequestRef) -> anyhow::Result<Self> {\n        let v = api_get::<PullRequest>(ref_.api_url()).await?;\n        Ok(v)\n    }\n\n    /// The flake URL referencing the branch of this PR\n    pub fn flake_url(&self) -> FlakeUrl {\n        // We cannot use `github:user/repo` syntax, because it doesn't support\n        // special characters in branch name. For that, we need to use the full\n        // git+https URL with url encoded `ref` query parameter.\n        FlakeUrl(format!(\n            \"git+https://github.com/{}?ref={}\",\n            self.head.repo.full_name,\n            urlencoding::encode(&self.head.ref_)\n        ))\n    }\n}\n\n/// Get an API response, parsing the response into the given type\nasync fn api_get<T>(url: String) -> anyhow::Result<T>\nwhere\n    T: serde::de::DeserializeOwned,\n{\n    let client = reqwest::Client::new();\n    let resp = client\n        .get(&url)\n        // Github API requires a user agent\n        .header(USER_AGENT, \"github.com/juspay/omnix\")\n        .send()\n        .await\n        .with_context(|| format!(\"cannot create request: {}\", &url))?;\n    if resp.status().is_success() {\n        let v = resp\n            .json::<T>()\n            .await\n            .with_context(|| format!(\"cannot parse response: {}\", &url))?;\n        Ok(v)\n    } else {\n        bail!(\"cannot make request: {}\", resp.status())\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/lib.rs",
    "content": "//! omnix-ci: CI for Nix projects\n#![warn(missing_docs)]\npub mod command;\npub mod config;\npub mod flake_ref;\npub mod github;\npub mod nix;\npub mod step;\n"
  },
  {
    "path": "crates/omnix-ci/src/nix/devour_flake.rs",
    "content": "//! Rust support for invoking <https://github.com/srid/devour-flake>\n\nuse lazy_static::lazy_static;\nuse nix_rs::{\n    flake::{functions::core::FlakeFn, url::FlakeUrl},\n    store::path::StorePath,\n};\nuse serde::{Deserialize, Serialize};\nuse std::{collections::HashMap, path::Path};\n\n/// Devour all outputs of a flake producing their store paths\npub struct DevourFlake;\n\nlazy_static! {\n    /// devour flake URL\n    static ref DEVOUR_FLAKE: FlakeUrl = {\n        let path = env!(\"DEVOUR_FLAKE\");\n        Into::<FlakeUrl>::into(Path::new(path)).with_attr(\"json\")\n    };\n}\n\nimpl FlakeFn for DevourFlake {\n    type Input = DevourFlakeInput;\n    type Output = DevourFlakeOutput;\n\n    fn flake() -> &'static FlakeUrl {\n        &DEVOUR_FLAKE\n    }\n\n    fn init(out: &mut DevourFlakeOutput) {\n        // Remove duplicates, which is possible in user's flake\n        // e.g., when doing `packages.foo = self'.packages.default`\n        out.out_paths.sort();\n        out.out_paths.dedup();\n    }\n}\n\n/// Input arguments to devour-flake\n#[derive(Serialize)]\npub struct DevourFlakeInput {\n    /// The flake whose outputs will be built\n    pub flake: FlakeUrl,\n    /// The systems it will build for. An empty list means all allowed systems.\n    pub systems: Option<FlakeUrl>,\n}\n\n/// Output of `devour-flake`\n#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct DevourFlakeOutput {\n    /// The built store paths\n    #[serde(rename = \"outPaths\")]\n    pub out_paths: Vec<StorePath>,\n\n    /// Output paths indexed by name (or pname) of the path if any\n    #[serde(rename = \"byName\")]\n    pub by_name: HashMap<String, StorePath>,\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/nix/lock.rs",
    "content": "//! Functions for working with `nix flake lock`.\n\nuse anyhow::{Ok, Result};\nuse nix_rs::{\n    command::NixCmd,\n    flake::{self, command::FlakeOptions, url::FlakeUrl},\n};\n\n/// Make sure that the `flake.lock` file is in sync.\npub async fn nix_flake_lock_check(nixcmd: &NixCmd, url: &FlakeUrl) -> Result<()> {\n    flake::command::lock(\n        nixcmd,\n        &FlakeOptions::default(),\n        &[\"--no-update-lock-file\"],\n        url,\n    )\n    .await?;\n    Ok(())\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/nix/mod.rs",
    "content": "//! Nix-specific types and functions\npub mod devour_flake;\npub mod lock;\n"
  },
  {
    "path": "crates/omnix-ci/src/step/build.rs",
    "content": "//! The build step\nuse clap::Parser;\nuse colored::Colorize;\nuse nix_rs::{\n    command::NixCmd,\n    flake::{functions::core::FlakeFn, url::FlakeUrl},\n    store::{command::NixStoreCmd, path::StorePath},\n};\nuse serde::{Deserialize, Serialize};\n\nuse crate::{\n    command::run::RunCommand,\n    config::subflake::SubflakeConfig,\n    nix::devour_flake::{DevourFlake, DevourFlakeInput, DevourFlakeOutput},\n};\n\n/// Represents a build step in the CI pipeline\n///\n/// It builds all flake outputs.\n///\n/// TODO: Should we use [`serde-bool`](https://docs.rs/serde-bool/latest/serde_bool/) to obviate that `Option` types in fields?\n#[derive(Debug, Clone, Deserialize)]\npub struct BuildStep {\n    /// Whether to enable this step\n    pub enable: bool,\n    /// Whether to pass `--impure` to `nix build`\n    #[serde(default)]\n    pub impure: Option<bool>,\n}\n\nimpl Default for BuildStep {\n    fn default() -> Self {\n        BuildStep {\n            enable: true,\n            impure: None,\n        }\n    }\n}\n\nimpl BuildStep {\n    /// Run this step\n    pub async fn run(\n        &self,\n        nixcmd: &NixCmd,\n        run_cmd: &RunCommand,\n        url: &FlakeUrl,\n        subflake: &SubflakeConfig,\n    ) -> anyhow::Result<BuildStepResult> {\n        // Run devour-flake to do the actual build.\n        tracing::info!(\n            \"{}\",\n            format!(\"⚒️  Building subflake: {}\", subflake.dir).bold()\n        );\n        let nix_args = subflake_extra_args(subflake);\n        let output = DevourFlake::call(\n            nixcmd,\n            self.impure.unwrap_or(false),\n            None,\n            None,\n            nix_args,\n            DevourFlakeInput {\n                flake: url.sub_flake_url(subflake.dir.clone()),\n                systems: run_cmd.systems.clone().map(|l| l.0),\n            },\n        )\n        .await?\n        .1;\n\n        let mut res = BuildStepResult {\n            devour_flake_output: output,\n            all_deps: None,\n        };\n\n        if run_cmd.steps_args.build_step_args.include_all_dependencies {\n            // Handle --include-all-dependencies\n            let all_paths = NixStoreCmd\n                .fetch_all_deps(&res.devour_flake_output.out_paths)\n                .await?;\n            res.all_deps = Some(all_paths);\n        }\n\n        Ok(res)\n    }\n}\n\n/// Extra args to pass to devour-flake\nfn subflake_extra_args(subflake: &SubflakeConfig) -> Vec<String> {\n    let mut args = vec![];\n\n    for (k, v) in &subflake.override_inputs {\n        args.extend([\n            \"--override-input\".to_string(),\n            k.to_string(),\n            v.0.to_string(),\n        ])\n    }\n\n    args\n}\n\n/// CLI arguments for [BuildStep]\n#[derive(Parser, Debug, Clone)]\npub struct BuildStepArgs {\n    /// Include build and runtime dependencies along with out paths in the result JSON\n    ///\n    /// By default, `om ci run` includes only the out paths. This option is\n    /// useful to explicitly push all dependencies to a cache.\n    #[clap(long, short = 'd')]\n    pub include_all_dependencies: bool,\n}\n\nimpl BuildStepArgs {\n    /// Convert this type back to the user-facing command line arguments\n    pub fn to_cli_args(&self) -> Vec<String> {\n        let mut args = vec![];\n\n        if self.include_all_dependencies {\n            args.push(\"--include-all-dependencies\".to_owned());\n        }\n\n        args\n    }\n}\n\n/// The result of the build step\n#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]\npub struct BuildStepResult {\n    /// Output of devour-flake\n    #[serde(flatten)]\n    pub devour_flake_output: DevourFlakeOutput,\n\n    /// All dependencies of the out paths, if available\n    #[serde(skip_serializing_if = \"Option::is_none\", rename = \"allDeps\")]\n    pub all_deps: Option<Vec<StorePath>>,\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/step/core.rs",
    "content": "//! All CI steps available\nuse clap::Parser;\nuse nix_rs::{\n    command::NixCmd,\n    flake::{system::System, url::FlakeUrl},\n};\nuse serde::{Deserialize, Serialize};\n\nuse super::{\n    build::{BuildStep, BuildStepArgs, BuildStepResult},\n    custom::CustomSteps,\n    flake_check::FlakeCheckStep,\n    lockfile::LockfileStep,\n};\nuse crate::command::run::RunCommand;\nuse crate::config::subflake::SubflakeConfig;\n\n/// CI steps to run\n///\n/// Contains some builtin steps, as well as custom steps (defined by user)\n#[derive(Debug, Default, Clone, Deserialize)]\npub struct Steps {\n    /// [LockfileStep]\n    #[serde(default, rename = \"lockfile\")]\n    pub lockfile_step: LockfileStep,\n\n    /// [BuildStep]\n    #[serde(default, rename = \"build\")]\n    pub build_step: BuildStep,\n\n    /// [FlakeCheckStep]\n    #[serde(default, rename = \"flake-check\")]\n    pub flake_check_step: FlakeCheckStep,\n\n    /// Custom steps\n    #[serde(default, rename = \"custom\")]\n    pub custom_steps: CustomSteps,\n}\n\n/// CLI arguments associated with [Steps]\n#[derive(Parser, Debug, Clone)]\npub struct StepsArgs {\n    /// [BuildStepArgs]\n    #[command(flatten)]\n    pub build_step_args: BuildStepArgs,\n}\n\n/// Results of [Steps]\n#[derive(Debug, Serialize, Deserialize, Clone, Default)]\npub struct StepsResult {\n    /// [BuildStepResult]\n    #[serde(rename = \"build\")]\n    pub build_step: Option<BuildStepResult>,\n}\n\nimpl Steps {\n    /// Run all CI steps\n    pub async fn run(\n        &self,\n        cmd: &NixCmd,\n        run_cmd: &RunCommand,\n        systems: &[System],\n        url: &FlakeUrl,\n        subflake: &SubflakeConfig,\n    ) -> anyhow::Result<StepsResult> {\n        let mut res = StepsResult::default();\n\n        if self.lockfile_step.enable {\n            self.lockfile_step.run(cmd, url, subflake).await?;\n        }\n\n        if self.build_step.enable {\n            let build_res = self.build_step.run(cmd, run_cmd, url, subflake).await?;\n            res.build_step = Some(build_res);\n        }\n\n        if self.flake_check_step.enable {\n            self.flake_check_step.run(cmd, url, subflake).await?;\n        }\n\n        self.custom_steps.run(cmd, systems, url, subflake).await?;\n\n        Ok(res)\n    }\n}\n\nimpl StepsArgs {\n    /// Convert this type back to the user-facing command line arguments\n    pub fn to_cli_args(&self) -> Vec<String> {\n        self.build_step_args.to_cli_args()\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/step/custom.rs",
    "content": "//! Custom steps in the CI pipeline\nuse colored::Colorize;\nuse nonempty::NonEmpty;\nuse serde::Deserialize;\nuse std::{collections::BTreeMap, future::Future, path::PathBuf};\n\nuse nix_rs::{\n    command::NixCmd,\n    flake::{\n        self,\n        system::System,\n        url::{attr::FlakeAttr, FlakeUrl},\n    },\n};\n\nuse crate::config::subflake::SubflakeConfig;\n\n/// Represents a custom step in the CI pipeline\n///\n/// All these commands are run in the same directory as the subflake\n#[derive(Debug, Clone, Deserialize)]\n#[serde(tag = \"type\")]\npub enum CustomStep {\n    /// A flake app to run\n    #[serde(rename = \"app\")]\n    FlakeApp {\n        /// Name of the app\n        #[serde(default)]\n        name: FlakeAttr,\n        /// Arguments to pass to the app\n        #[serde(default)]\n        args: Vec<String>,\n        /// Whitelist of systems to run on\n        systems: Option<Vec<System>>,\n    },\n\n    /// An arbitrary command to run in the devshell\n    #[serde(rename = \"devshell\")]\n    FlakeDevShellCommand {\n        /// Name of the devShell\n        #[serde(default)]\n        name: FlakeAttr,\n        /// The command to run inside of devshell\n        command: NonEmpty<String>,\n        /// Whitelist of systems to run on\n        systems: Option<Vec<System>>,\n    },\n}\n\nimpl CustomStep {\n    /// Run this step\n    pub async fn run(\n        &self,\n        nixcmd: &NixCmd,\n        url: &FlakeUrl,\n        subflake: &SubflakeConfig,\n    ) -> anyhow::Result<()> {\n        with_writeable_flake_dir(nixcmd, url, |flake_path| async move {\n            self.run_on_local_path(nixcmd, flake_path, subflake).await\n        })\n        .await\n    }\n\n    /// Like [run] but runs on a flake that is known to be at a local path\n    async fn run_on_local_path(\n        &self,\n        nixcmd: &NixCmd,\n        flake_path: PathBuf,\n        subflake: &SubflakeConfig,\n    ) -> anyhow::Result<()> {\n        let path = flake_path.join(&subflake.dir);\n        tracing::info!(\"Running custom step under: {:}\", &path.display());\n\n        let pwd_flake = FlakeUrl::from(PathBuf::from(\".\"));\n        let flake_opts = flake::command::FlakeOptions {\n            override_inputs: subflake.override_inputs.clone(),\n            current_dir: Some(path.clone()),\n            no_write_lock_file: false,\n        };\n\n        match self {\n            CustomStep::FlakeApp { name, args, .. } => {\n                flake::command::run(\n                    nixcmd,\n                    &flake_opts,\n                    &pwd_flake.with_attr(&name.get_name()),\n                    args.clone(),\n                )\n                .await?;\n            }\n            CustomStep::FlakeDevShellCommand { name, command, .. } => {\n                flake::command::develop(\n                    nixcmd,\n                    &flake_opts,\n                    &pwd_flake.with_attr(&name.get_name()),\n                    command.clone(),\n                )\n                .await?;\n            }\n        }\n        Ok(())\n    }\n\n    fn can_run_on(&self, systems: &[System]) -> bool {\n        match self.get_systems() {\n            Some(systems_whitelist) => systems_whitelist.iter().any(|s| systems.contains(s)),\n            None => true,\n        }\n    }\n\n    fn get_systems(&self) -> &Option<Vec<System>> {\n        match self {\n            CustomStep::FlakeApp { systems, .. } => systems,\n            CustomStep::FlakeDevShellCommand { systems, .. } => systems,\n        }\n    }\n}\n\n/// A collection of custom steps\n#[derive(Debug, Clone, Default, Deserialize)]\npub struct CustomSteps(BTreeMap<String, CustomStep>);\n\nimpl CustomSteps {\n    /// Run all custom steps\n    pub async fn run(\n        &self,\n        nixcmd: &NixCmd,\n        systems: &[System],\n        url: &FlakeUrl,\n        subflake: &SubflakeConfig,\n    ) -> anyhow::Result<()> {\n        for (name, step) in &self.0 {\n            if step.can_run_on(systems) {\n                tracing::info!(\"{}\", format!(\"🏗  Running custom step: {}\", name).bold());\n                step.run(nixcmd, url, subflake).await?;\n            } else {\n                tracing::info!(\n                  \"{}\",\n                  format!(\n                      \"🏗  Skipping custom step {} because it's not whitelisted for the current system: {:?}\",\n                      name,\n                      systems.iter().map(|s| s.to_string()).collect::<Vec<_>>()\n                  )\n                  .yellow()\n              );\n            }\n        }\n        Ok(())\n    }\n}\n\n/// Call the given function with a (write-able) local path equivalent to the given URL\n///\n/// The flake is retrieved locally, and stored in a temp directory is created if necessary.\n///\n/// Two reasons for copying to a temp (and writeable) directory:\n/// 1. `nix run` does not work reliably on store paths (`/nix/store/**`)\n/// 2. `nix develop -c ...` often requires mutable flake directories\nasync fn with_writeable_flake_dir<F, Fut>(\n    nixcmd: &NixCmd,\n    url: &FlakeUrl,\n    f: F,\n) -> anyhow::Result<()>\nwhere\n    F: FnOnce(PathBuf) -> Fut,\n    Fut: Future<Output = anyhow::Result<()>>,\n{\n    // First, ensure that flake is locally available.\n    let local_path = match url.as_local_path() {\n        Some(local_path) => local_path.to_path_buf(),\n        None => url.as_local_path_or_fetch(nixcmd).await?,\n    };\n\n    // Then, ensure that it is writeable by the user\n    let read_only = local_path.metadata()?.permissions().readonly();\n    let path = if read_only {\n        // Two reasons for copying to a temp location:\n        // 1. `nix run` does not work reliably on store paths\n        // 2. `nix develop -c ...` often require mutable flake directories\n        let target_path = tempfile::Builder::new()\n            .prefix(\"om-ci-\")\n            .tempdir()?\n            .path()\n            .join(\"flake\");\n        omnix_common::fs::copy_dir_all(&local_path, &target_path).await?;\n        target_path\n    } else {\n        local_path\n    };\n\n    // Finally, call the function with the path\n    f(path).await\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/step/flake_check.rs",
    "content": "//! The cachix step\nuse colored::Colorize;\nuse nix_rs::{\n    command::NixCmd,\n    flake::{self, command::FlakeOptions, url::FlakeUrl},\n};\nuse serde::Deserialize;\n\nuse crate::config::subflake::SubflakeConfig;\n\n/// Run `nix flake check`\n///\n/// Note: `nix build ...` does not evaluate all the checks that `nix flake check` does. So, enabling this steps allows `om ci` to run those evaluation checks.\n#[derive(Debug, Clone, Default, Deserialize)]\npub struct FlakeCheckStep {\n    /// Whether to enable this step\n    ///\n    /// Disabled by default, since only a handful of flakes need this (for others, it will unnecessarily slow down the build)\n    pub enable: bool,\n}\n\nimpl FlakeCheckStep {\n    /// Run this step\n    pub async fn run(\n        &self,\n        nixcmd: &NixCmd,\n        url: &FlakeUrl,\n        subflake: &SubflakeConfig,\n    ) -> anyhow::Result<()> {\n        tracing::info!(\n            \"{}\",\n            format!(\"🩺 Running flake check on: {}\", subflake.dir).bold()\n        );\n        let sub_flake_url = url.sub_flake_url(subflake.dir.clone());\n        let opts = FlakeOptions {\n            override_inputs: subflake.override_inputs.clone(),\n            ..Default::default()\n        };\n        flake::command::check(nixcmd, &opts, &sub_flake_url).await?;\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/step/lockfile.rs",
    "content": "//! The lockfile step\nuse colored::Colorize;\nuse nix_rs::{command::NixCmd, flake::url::FlakeUrl};\nuse serde::Deserialize;\n\nuse crate::{config::subflake::SubflakeConfig, nix};\n\n/// Check that `flake.lock` is not out of date.\n#[derive(Debug, Clone, Deserialize)]\npub struct LockfileStep {\n    /// Whether to enable this step\n    pub enable: bool,\n}\n\nimpl Default for LockfileStep {\n    fn default() -> Self {\n        LockfileStep { enable: true }\n    }\n}\n\nimpl LockfileStep {\n    /// Run this step\n    pub async fn run(\n        &self,\n        nixcmd: &NixCmd,\n        url: &FlakeUrl,\n        subflake: &SubflakeConfig,\n    ) -> anyhow::Result<()> {\n        if subflake.override_inputs.is_empty() {\n            tracing::info!(\n                \"{}\",\n                format!(\"🫀 Checking that {}/flake.lock is up-to-date\", subflake.dir).bold()\n            );\n            let sub_flake_url = url.sub_flake_url(subflake.dir.clone());\n            nix::lock::nix_flake_lock_check(nixcmd, &sub_flake_url).await?;\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/omnix-ci/src/step/mod.rs",
    "content": "//! CI is broken down into various 'steps'.\npub mod build;\npub mod core;\npub mod custom;\npub mod flake_check;\npub mod lockfile;\n"
  },
  {
    "path": "crates/omnix-cli/Cargo.toml",
    "content": "[package]\nname = \"omnix-cli\"\nversion = \"1.3.2\"\nedition = \"2021\"\ndefault-run = \"om\"\n# NOTE: The 'description' here will be printed in `om` CLI banner (thanks to `clap` crate)\ndescription = \"omnix: a developer-friendly companion for Nix <https://omnix.page>\"\nhomepage = \"https://omnix.page\"\nrepository = \"https://github.com/juspay/omnix\"\nlicense = \"AGPL-3.0-only\"\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n[[bin]]\nname = \"om\"\npath = \"src/main.rs\"\n\n[dependencies]\nanyhow = { workspace = true }\nclap = { workspace = true }\nclap-verbosity-flag = { workspace = true }\ncolored = { workspace = true }\nhuman-panic = { workspace = true }\nomnix-ci = { workspace = true }\nomnix-health = { workspace = true }\nnix_rs = { workspace = true }\nomnix-common = { workspace = true }\nomnix-init = { workspace = true }\nomnix-develop = { workspace = true }\ntabled = { workspace = true }\ntokio = { workspace = true }\ntracing = { workspace = true }\nclap_complete = { workspace = true }\nclap_complete_nushell = { workspace = true }\nserde = { workspace = true }\nserde_json = { workspace = true }\n\n[dev-dependencies]\nanyhow = { workspace = true }\nassert_cmd = \"2\"\nassert_fs = \"1\"\nassert_matches = \"1.5\"\nctor = \"0.2\"\npredicates = \"3\"\nregex = \"1.9\"\n"
  },
  {
    "path": "crates/omnix-cli/crate.nix",
    "content": "{ pkgs\n, lib\n, ...\n}:\n\nlet\n  inherit (pkgs) stdenv pkgsStatic;\nin\n{\n  autoWire = [ ];\n  crane = {\n    args = {\n      nativeBuildInputs = [\n        # Packages from `pkgsStatic` require cross-compilation support for the target platform,\n        # which is not yet available for `x86_64-apple-darwin` in nixpkgs. Upon trying to evaluate\n        # a static package for `x86_64-apple-darwin`, you may see an error like:\n        #\n        # > error: don't yet have a `targetPackages.darwin.LibsystemCross for x86_64-apple-darwin`\n        (if (stdenv.isDarwin && stdenv.isAarch64) then pkgsStatic.libiconv else pkgs.libiconv)\n        pkgs.pkg-config\n      ];\n      buildInputs = lib.optionals pkgs.stdenv.isLinux [\n        pkgsStatic.openssl\n      ];\n\n      # Disable tests due to sandboxing issues; we run them on CI\n      # instead.\n      doCheck = false;\n      meta = {\n        description = \"Command-line interface for Omnix\";\n        mainProgram = \"om\";\n      };\n      CARGO_BUILD_RUSTFLAGS = \"-C target-feature=+crt-static\";\n\n      hardeningDisable = [ \"fortify\" ]; # https://github.com/NixOS/nixpkgs/issues/18995#issuecomment-249748307\n    } //\n    lib.optionalAttrs (stdenv.isLinux && stdenv.isAarch64) {\n      CARGO_BUILD_TARGET = \"aarch64-unknown-linux-musl\";\n    } //\n    lib.optionalAttrs (stdenv.isLinux && stdenv.isx86_64) {\n      CARGO_BUILD_TARGET = \"x86_64-unknown-linux-musl\";\n    };\n  };\n}\n"
  },
  {
    "path": "crates/omnix-cli/src/args.rs",
    "content": "use clap::Parser;\nuse clap_verbosity_flag::{InfoLevel, Verbosity};\n\nuse crate::command::core::Command;\n\n/// Omnix CLI entrypoint <https://omnix.page/>\n#[derive(Parser, Debug)]\n#[command(version, about)]\npub struct Args {\n    #[command(flatten)]\n    pub verbosity: Verbosity<InfoLevel>,\n\n    #[clap(subcommand)]\n    pub command: Command,\n}\n"
  },
  {
    "path": "crates/omnix-cli/src/command/ci.rs",
    "content": "use clap::Parser;\nuse omnix_ci::command::core::Command;\n\n/// Build all outputs of the flake\n#[derive(Parser, Debug)]\npub struct CICommand {\n    #[clap(subcommand)]\n    command: Option<Command>,\n}\n\nimpl CICommand {\n    /// Run this sub-command\n    pub async fn run(&self) -> anyhow::Result<()> {\n        self.command().run().await?;\n        Ok(())\n    }\n\n    /// Get the command to run\n    ///\n    /// If the user has not provided one, return the build command by default.\n    fn command(&self) -> Command {\n        self.command.clone().unwrap_or_default()\n    }\n}\n"
  },
  {
    "path": "crates/omnix-cli/src/command/completion.rs",
    "content": "use clap::CommandFactory;\nuse clap::Parser;\nuse clap_complete::generate;\n\n/// Generates shell completion scripts\n#[derive(Parser, Debug)]\npub struct CompletionCommand {\n    #[arg(value_enum)]\n    shell: Shell2,\n}\n\nimpl CompletionCommand {\n    pub fn run(&self) -> anyhow::Result<()> {\n        generate_completion(self.shell);\n        Ok(())\n    }\n}\n\n/// Like `clap::Shell`, but with an additional variant for Nushell\n#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]\n#[non_exhaustive]\nenum Shell2 {\n    Bash,\n    Elvish,\n    Fish,\n    PowerShell,\n    Zsh,\n    Nushell,\n}\n\nimpl Shell2 {\n    fn nushell_or(self) -> Result<clap_complete::Shell, clap_complete_nushell::Nushell> {\n        match self {\n            Shell2::Nushell => Err(clap_complete_nushell::Nushell),\n            Shell2::Bash => Ok(clap_complete::Shell::Bash),\n            Shell2::Elvish => Ok(clap_complete::Shell::Elvish),\n            Shell2::Fish => Ok(clap_complete::Shell::Fish),\n            Shell2::PowerShell => Ok(clap_complete::Shell::PowerShell),\n            Shell2::Zsh => Ok(clap_complete::Shell::Zsh),\n        }\n    }\n}\n\nfn generate_completion(shell: Shell2) {\n    let cmd = &mut crate::args::Args::command();\n    let out = &mut std::io::stdout();\n    match shell.nushell_or() {\n        Ok(shell) => generate(shell, cmd, \"om\", out),\n        Err(shell) => generate(shell, cmd, \"om\", out),\n    }\n}\n"
  },
  {
    "path": "crates/omnix-cli/src/command/core.rs",
    "content": "use clap::Subcommand;\n\n#[derive(Subcommand, Debug)]\npub enum Command {\n    Show(super::show::ShowCommand),\n\n    Init(super::init::InitCommand),\n\n    #[clap(alias = \"hack\")]\n    Develop(super::develop::DevelopCommand),\n\n    CI(super::ci::CICommand),\n\n    Health(super::health::HealthCommand),\n\n    Completion(super::completion::CompletionCommand),\n}\n\nimpl Command {\n    pub async fn run(&self) -> anyhow::Result<()> {\n        if !matches!(self, Command::Completion(_)) && !omnix_common::check::nix_installed() {\n            tracing::error!(\"Nix is not installed: https://nixos.asia/en/install\");\n            std::process::exit(1);\n        }\n\n        match self {\n            Command::Show(cmd) => cmd.run().await,\n            Command::Init(cmd) => cmd.run().await,\n            Command::Develop(cmd) => cmd.run().await,\n            Command::CI(cmd) => cmd.run().await,\n            Command::Health(cmd) => cmd.run().await,\n            Command::Completion(cmd) => cmd.run(),\n        }\n    }\n}\n"
  },
  {
    "path": "crates/omnix-cli/src/command/develop.rs",
    "content": "use clap::Parser;\nuse nix_rs::{command::NixCmd, flake::url::FlakeUrl};\nuse omnix_common::config::OmConfig;\n\n/// Prepare to develop on a flake project\n#[derive(Parser, Debug)]\npub struct DevelopCommand {\n    /// Directory of the project\n    #[arg(name = \"DIR\", default_value = \".\")]\n    flake_shell: FlakeUrl,\n\n    /// The stage to run in. If not provided, runs all stages.\n    #[arg(long, value_enum)]\n    stage: Option<Stage>,\n\n    /// Nix command global options\n    #[command(flatten)]\n    pub nixcmd: NixCmd,\n}\n\n/// The stage to run in\n#[derive(clap::ValueEnum, Debug, Clone)]\nenum Stage {\n    /// Stage before Nix shell is invoked.\n    PreShell,\n\n    /// Stage after Nix shell is successfully invoked.\n    PostShell,\n}\n\nimpl DevelopCommand {\n    pub async fn run(&self) -> anyhow::Result<()> {\n        let flake = self.flake_shell.without_attr();\n\n        let om_config = OmConfig::get(&self.nixcmd, &flake).await?;\n\n        tracing::info!(\"⌨️  Preparing to develop project: {:}\", &flake);\n        let prj = omnix_develop::core::Project::new(flake, om_config).await?;\n        match self.stage {\n            Some(Stage::PreShell) => omnix_develop::core::develop_on_pre_shell(&prj).await?,\n            Some(Stage::PostShell) => omnix_develop::core::develop_on_post_shell(&prj).await?,\n            None => omnix_develop::core::develop_on(&prj).await?,\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/omnix-cli/src/command/health.rs",
    "content": "use clap::Parser;\nuse nix_rs::{command::NixCmd, flake::url::FlakeUrl};\nuse omnix_health::{run_all_checks_with, NixHealth};\n\n/// Display the health of your Nix dev environment\n#[derive(Parser, Debug)]\npub struct HealthCommand {\n    /// Use `om.health` configuration from the given flake\n    #[arg(name = \"FLAKE\")]\n    pub flake_url: Option<FlakeUrl>,\n\n    /// Dump the config schema of the health checks (useful when adding them to\n    /// a flake.nix)\n    #[arg(long = \"dump-schema\")]\n    pub dump_schema: bool,\n\n    /// Print output in JSON\n    #[arg(long)]\n    json: bool,\n\n    /// Nix command global options\n    #[command(flatten)]\n    pub nixcmd: NixCmd,\n}\n\nimpl HealthCommand {\n    pub async fn run(&self) -> anyhow::Result<()> {\n        if self.dump_schema {\n            println!(\"{}\", NixHealth::schema()?);\n        } else {\n            let checks =\n                run_all_checks_with(&self.nixcmd, self.flake_url.clone(), self.json).await?;\n            let exit_code = NixHealth::print_report_returning_exit_code(&checks, self.json).await?;\n            if exit_code != 0 {\n                std::process::exit(exit_code);\n            }\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/omnix-cli/src/command/init.rs",
    "content": "use std::{collections::HashMap, path::PathBuf, str::FromStr};\n\nuse clap::Parser;\nuse nix_rs::{command::NixCmd, config::NixConfig, flake::url::FlakeUrl};\nuse serde_json::Value;\n\n/// Initialize a new flake project\n#[derive(Parser, Debug)]\npub struct InitCommand {\n    /// Where to create the template\n    #[arg(\n        name = \"OUTPUT_DIR\",\n        short = 'o',\n        long = \"output\",\n        required_unless_present = \"test\"\n    )]\n    path: Option<PathBuf>,\n\n    /// The flake from which to initialize the template to use\n    ///\n    /// Defaults to builtin registry of flake templates.\n    #[arg(name = \"FLAKE_URL\")]\n    flake: Option<FlakeUrl>,\n\n    /// Parameter values to use for the template by default.\n    #[arg(long = \"params\")]\n    params: Option<Params>,\n\n    /// Whether to disable all prompting, making the command non-interactive\n    #[arg(long = \"non-interactive\")]\n    non_interactive: bool,\n\n    /// Run template tests, instead of initializing the template\n    #[arg(\n        long = \"test\",\n        requires = \"FLAKE_URL\",\n        conflicts_with = \"non_interactive\",\n        conflicts_with = \"params\",\n        conflicts_with = \"OUTPUT_DIR\"\n    )]\n    test: bool,\n\n    /// Nix command global options\n    #[command(flatten)]\n    pub nixcmd: NixCmd,\n}\n\nimpl InitCommand {\n    pub async fn run(&self) -> anyhow::Result<()> {\n        if self.test {\n            let cfg = NixConfig::get().await.as_ref()?;\n            omnix_init::core::run_tests(\n                &self.nixcmd,\n                &cfg.system.value,\n                &self.registry_choose().await?,\n            )\n            .await?;\n        } else {\n            let path = self.path.as_ref().unwrap(); // unwrap is okay, because of `required_unless_present`\n            if path.exists() {\n                // Make sure that the directory does not already exist. We don't risk mutating accidentally incorrect location!\n                anyhow::bail!(\"Output directory already exists: {}\", path.display());\n            }\n\n            let params = self\n                .params\n                .as_ref()\n                .map_or_else(HashMap::new, |hm| hm.0.clone());\n            omnix_init::core::run(\n                &self.nixcmd,\n                path,\n                &self.registry_choose().await?,\n                &params,\n                self.non_interactive,\n            )\n            .await?;\n        }\n        Ok(())\n    }\n    async fn registry_choose(&self) -> anyhow::Result<FlakeUrl> {\n        match self.flake {\n            Some(ref flake) => Ok(flake.clone()),\n            None => omnix_init::core::select_from_registry(&self.nixcmd).await,\n        }\n    }\n}\n\n/// A map of parameter values\n#[derive(Clone, Debug, Default)]\nstruct Params(HashMap<String, Value>);\n\n/// Convenience for passing JSON in command line\nimpl FromStr for Params {\n    type Err = serde_json::Error;\n\n    fn from_str(s: &str) -> Result<Self, Self::Err> {\n        let map: HashMap<String, Value> = serde_json::from_str(s)?;\n        Ok(Params(map))\n    }\n}\n"
  },
  {
    "path": "crates/omnix-cli/src/command/mod.rs",
    "content": "pub mod ci;\npub mod completion;\npub mod core;\npub mod develop;\npub mod health;\npub mod init;\npub mod show;\n"
  },
  {
    "path": "crates/omnix-cli/src/command/show.rs",
    "content": "use std::io::IsTerminal;\n\nuse anyhow::Context;\nuse clap::Parser;\nuse colored::Colorize;\nuse nix_rs::{\n    command::NixCmd,\n    config::NixConfig,\n    flake::{outputs::FlakeOutputs, url::FlakeUrl, Flake},\n};\nuse tabled::{\n    settings::{location::ByColumnName, Color, Modify, Style},\n    Table, Tabled,\n};\n\n/// Inspect the outputs of a flake\n#[derive(Parser, Debug)]\npub struct ShowCommand {\n    /// The flake to show outputs for\n    #[arg(name = \"FLAKE\")]\n    pub flake_url: FlakeUrl,\n\n    /// Nix command global options\n    #[command(flatten)]\n    pub nixcmd: NixCmd,\n}\n\n/// Tabular representation of a set of flake outputs (eg: `packages.*`)\npub struct FlakeOutputTable {\n    /// Rows of the table\n    pub rows: Vec<Row>,\n    /// Title of the table\n    pub title: String,\n    /// Command to run the outputs in the `name` column\n    pub command: Option<String>,\n}\n\nimpl FlakeOutputTable {\n    /// Convert the table to a [Table] struct\n    fn to_tabled(&self) -> Table {\n        let mut table = Table::new(&self.rows);\n        table.with(Style::rounded());\n        if std::io::stdout().is_terminal() {\n            table.with(Modify::new(ByColumnName::new(\"name\")).with(Color::BOLD));\n        };\n        table\n    }\n\n    /// Print the table to stdout\n    pub fn print(&self) {\n        if self.rows.is_empty() {\n            return;\n        }\n        print!(\"{}\", self.title.blue().bold());\n\n        if let Some(command) = &self.command {\n            println!(\" ({})\", command.green().bold());\n        } else {\n            // To ensure the table name and the table are on separate lines\n            println!();\n        }\n\n        println!(\"{}\", self.to_tabled());\n        println!();\n    }\n}\n\n/// Row in a [FlakeOutputTable]\n#[derive(Tabled)]\npub struct Row {\n    /// Name of the output\n    pub name: String,\n    /// Description of the output\n    pub description: String,\n}\n\nimpl Row {\n    /// Convert a [FlakeOutputs] to a vector of [Row]s\n    pub fn vec_from_flake_output(output: &FlakeOutputs) -> Vec<Row> {\n        output\n            .get_attrset_of_val()\n            .into_iter()\n            .map(|(name, val)| Row {\n                name,\n                description: val\n                    .short_description\n                    .filter(|s| !s.is_empty())\n                    .unwrap_or(String::from(\"N/A\"))\n                    .to_owned(),\n            })\n            .collect()\n    }\n}\n\nimpl ShowCommand {\n    pub async fn run(&self) -> anyhow::Result<()> {\n        let nix_config = NixConfig::get().await.as_ref()?;\n        let system = &nix_config.system.value;\n        let flake = Flake::from_nix(&self.nixcmd, nix_config, self.flake_url.clone())\n            .await\n            .with_context(|| \"Unable to fetch flake\")?;\n\n        let print_flake_output_table = |title: &str, keys: &[&str], command: Option<String>| {\n            FlakeOutputTable {\n                rows: flake\n                    .output\n                    .get_by_path(keys)\n                    .map_or(vec![], Row::vec_from_flake_output),\n                title: title.to_string(),\n                command,\n            }\n            .print();\n        };\n\n        print_flake_output_table(\n            \"📦 Packages\",\n            &[\"packages\", system.as_ref()],\n            Some(format!(\"nix build {}#<name>\", self.flake_url)),\n        );\n        print_flake_output_table(\n            \"🐚 Devshells\",\n            &[\"devShells\", system.as_ref()],\n            Some(format!(\"nix develop {}#<name>\", self.flake_url)),\n        );\n        print_flake_output_table(\n            \"🚀 Apps\",\n            &[\"apps\", system.as_ref()],\n            Some(format!(\"nix run {}#<name>\", self.flake_url)),\n        );\n        print_flake_output_table(\n            \"🔍 Checks\",\n            &[\"checks\", system.as_ref()],\n            Some(\"nix flake check\".to_string()),\n        );\n\n        print_flake_output_table(\n            \"🐧 NixOS Configurations\",\n            &[\"nixosConfigurations\"],\n            Some(format!(\n                \"nixos-rebuild switch --flake {}#<name>\",\n                self.flake_url\n            )),\n        );\n        print_flake_output_table(\n            \"🍏 Darwin Configurations\",\n            &[\"darwinConfigurations\"],\n            Some(format!(\n                \"darwin-rebuild switch --flake {}#<name>\",\n                self.flake_url\n            )),\n        );\n        print_flake_output_table(\"🔧 NixOS Modules\", &[\"nixosModules\"], None);\n        print_flake_output_table(\n            \"🐳 Docker Images\",\n            &[\"dockerImages\"],\n            Some(format!(\"nix build {}#dockerImages.<name>\", self.flake_url)),\n        );\n        print_flake_output_table(\"🎨 Overlays\", &[\"overlays\"], None);\n        print_flake_output_table(\n            \"📝 Templates\",\n            &[\"templates\"],\n            Some(format!(\"nix flake init -t {}#<name>\", self.flake_url)),\n        );\n        print_flake_output_table(\"📜 Schemas\", &[\"schemas\"], None);\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/omnix-cli/src/lib.rs",
    "content": "pub mod args;\npub mod command;\n"
  },
  {
    "path": "crates/omnix-cli/src/main.rs",
    "content": "use clap::Parser;\n\n#[tokio::main]\nasync fn main() -> anyhow::Result<()> {\n    // To avoid clippy warning\n    // error: use of deprecated type alias `std::panic::PanicInfo`: use `PanicHookInfo` instead\n    #[allow(deprecated)]\n    {\n        human_panic::setup_panic!();\n    }\n\n    let args = omnix_cli::args::Args::parse();\n    let verbose = args.verbosity.log_level() > Some(clap_verbosity_flag::Level::Info);\n    omnix_common::logging::setup_logging(&args.verbosity, !verbose);\n    tracing::debug!(\"Args: {:?}\", args);\n    args.command.run().await\n}\n"
  },
  {
    "path": "crates/omnix-cli/tests/command/ci.rs",
    "content": "use std::path::{Path, PathBuf};\n\nuse anyhow::bail;\nuse nix_rs::store::path::StorePath;\nuse regex::Regex;\nuse serde::de::DeserializeOwned;\nuse serde_json::Value;\n\nuse super::core::om;\n\n/// Run `om ci run` passing given arguments, returning its stdout (parsed).\nasync fn om_ci_run(args: &[&str]) -> anyhow::Result<StorePath> {\n    let mut cmd = om()?;\n    cmd.arg(\"ci\").arg(\"run\").args(args);\n\n    let output = cmd.output()?;\n    if !output.status.success() {\n        bail!(\n            \"Failed to run `om ci run`:\\n{}\",\n            String::from_utf8_lossy(&output.stderr).to_string(),\n        );\n    }\n    let stdout = String::from_utf8_lossy(&output.stdout);\n    let out = StorePath::new(PathBuf::from(stdout.trim()));\n    Ok(out)\n}\n\n#[tokio::test]\n/// Run `om ci build` and check if the stdout consists of only /nix/store/* paths\nasync fn build_flake_output() -> anyhow::Result<()> {\n    let out =\n        om_ci_run(&[\"github:srid/haskell-multi-nix/c85563721c388629fa9e538a1d97274861bc8321\"])\n            .await?;\n\n    assert!(\n        out.as_path().starts_with(\"/nix/store/\"),\n        \"Unexpected line in stdout: {}\",\n        out\n    );\n\n    Ok(())\n}\n\n#[tokio::test]\n/// A simple test, without config\nasync fn test_haskell_multi_nix() -> anyhow::Result<()> {\n    let out =\n        om_ci_run(&[\"github:srid/haskell-multi-nix/c85563721c388629fa9e538a1d97274861bc8321\"])\n            .await?;\n    let v: Value = serde_json::from_reader(std::fs::File::open(&out)?)?;\n    let paths: Vec<PathBuf> = lookup_path(&v, &[\"result\", \"ROOT\", \"build\", \"outPaths\"]).unwrap();\n    let expected = vec![\n        \"/nix/store/3x2kpymc1qmd05da20wnmdyam38jkl7s-ghc-shell-for-packages-0\",\n        \"/nix/store/dzhf0i3wi69568m5nvyckck8bbs9yrfd-foo-0.1.0.0\",\n        \"/nix/store/hsj8mwn9vzlyaxzmwyf111scisnjhlkb-bar-0.1.0.0\",\n        \"/nix/store/hsj8mwn9vzlyaxzmwyf111scisnjhlkb-bar-0.1.0.0/bin/bar\",\n    ]\n    .into_iter()\n    .map(|s| PathBuf::from(s.to_string()))\n    .collect::<Vec<_>>();\n    assert_same_drvs(paths, expected);\n    Ok(())\n}\n\n#[tokio::test]\nasync fn test_haskell_multi_nix_all_dependencies() -> anyhow::Result<()> {\n    let out = om_ci_run(&[\n        \"--include-all-dependencies\",\n        \"github:srid/haskell-multi-nix/c85563721c388629fa9e538a1d97274861bc8321\",\n    ])\n    .await?;\n    let v: Value = serde_json::from_reader(std::fs::File::open(&out)?)?;\n    let paths: Vec<PathBuf> = lookup_path(&v, &[\"result\", \"ROOT\", \"build\", \"allDeps\"]).unwrap();\n    // Since the number of dependencies is huge, we just check for the presence of system-independent\n    // source of the `foo` sub-package in `haskell-multi-nix`.\n    let expected = PathBuf::from(\"/nix/store/bpybsny4gd5jnw0lvk5khpq7md6nwg5f-source-foo\");\n    assert!(paths.contains(&expected));\n    Ok(())\n}\n\n#[tokio::test]\n/// Whether `--override-input` passes CI successfully\nasync fn test_haskell_multi_nix_override_input() -> anyhow::Result<()> {\n    let _out = om_ci_run(&[\n        \"github:srid/haskell-multi-nix/c85563721c388629fa9e538a1d97274861bc8321\",\n        \"--\",\n        \"--override-input\",\n        \"haskell-flake\",\n        // haskell-flake 0.4 release\n        \"github:srid/haskell-flake/c8622c8a259e18e0a1919462ce885380108a723c\",\n    ])\n    .await?;\n    Ok(())\n}\n\n#[tokio::test]\n/// A test, with config\nasync fn test_services_flake() -> anyhow::Result<()> {\n    let out = om_ci_run(&[\n        \"github:juspay/services-flake/23cf162387af041035072ee4a9de20f8408907cb#default.simple-example\",\n    ])\n    .await?;\n    let v: Value = serde_json::from_reader(std::fs::File::open(&out)?)?;\n    let paths: Vec<PathBuf> =\n        lookup_path(&v, &[\"result\", \"simple-example\", \"build\", \"outPaths\"]).unwrap();\n    let expected = vec![\n        \"/nix/store/ib83flb2pqjb416qrjbs4pqhifa3hhs4-default-test\",\n        \"/nix/store/l9c8y2xx2iffk8l1ipp4mkval8wl8paa-default\",\n        \"/nix/store/pj2l11lc4kai6av32hgfsrsvmga7vkhf-nix-shell\",\n    ]\n    .into_iter()\n    .map(|s| PathBuf::from(s.to_string()))\n    .collect::<Vec<_>>();\n    assert_same_drvs(paths, expected);\n    Ok(())\n}\n\npub fn assert_same_drvs(drvs1: Vec<PathBuf>, drvs2: Vec<PathBuf>) {\n    assert_eq!(drvs1.len(), drvs2.len());\n    let mut drv1 = drvs1\n        .into_iter()\n        .map(|d| without_hash(&d))\n        .collect::<Vec<_>>();\n    let mut drv2 = drvs2\n        .into_iter()\n        .map(|d| without_hash(&d))\n        .collect::<Vec<_>>();\n    drv1.sort();\n    drv2.sort();\n    assert_eq!(drv1, drv2);\n}\n\npub fn without_hash(out_path: &Path) -> String {\n    let re = Regex::new(r\".+\\-(.+)\").unwrap();\n    let captures = re.captures(out_path.to_str().unwrap()).unwrap();\n    captures.get(1).unwrap().as_str().to_string()\n}\n\n/// Lookup a path in the [`serde_json::Value`]\nfn lookup_path<T>(v: &Value, path: &[&str]) -> Option<T>\nwhere\n    T: DeserializeOwned,\n{\n    match path {\n        [] => None,\n        [key] => v\n            .get(key)\n            .and_then(|v| serde_json::from_value(v.clone()).ok()),\n        [key, rest @ ..] => v.get(key).and_then(|v| lookup_path(v, rest)),\n    }\n}\n"
  },
  {
    "path": "crates/omnix-cli/tests/command/core.rs",
    "content": "use assert_cmd::Command;\n\n/// `om --help` works\n#[test]\nfn om_help() -> anyhow::Result<()> {\n    om()?.arg(\"--help\").assert().success();\n    Ok(())\n}\n\n/// Return the [Command] pointing to the `om` cargo bin\npub(crate) fn om() -> anyhow::Result<Command> {\n    Ok(Command::cargo_bin(\"om\")?)\n}\n"
  },
  {
    "path": "crates/omnix-cli/tests/command/health.rs",
    "content": "use predicates::{prelude::*, str::contains};\n\nuse super::core::om;\n\n/// `om health` runs, and succeeds.\n#[test]\nfn om_health() -> anyhow::Result<()> {\n    om()?\n        .arg(\"health\")\n        .assert()\n        .success()\n        .stderr(contains(\"All checks passed\").or(contains(\"Required checks passed\")));\n    Ok(())\n}\n"
  },
  {
    "path": "crates/omnix-cli/tests/command/init.rs",
    "content": "use nix_rs::{command::NixCmd, config::NixConfig};\n\n/// `om init` runs and successfully initializes a template\n#[tokio::test]\nasync fn om_init() -> anyhow::Result<()> {\n    let registry = omnix_init::registry::get(NixCmd::get().await)\n        .await\n        .as_ref()?;\n    let cfg = NixConfig::get().await.as_ref()?;\n    let current_system = &cfg.system.value;\n    for url in registry.0.values() {\n        // TODO: Refactor(DRY) with src/core.rs:run_tests\n        // TODO: Make this test (and other tests) use tracing!\n        println!(\"🕍 Testing template: {}\", url);\n        let templates = omnix_init::config::load_templates(NixCmd::get().await, url).await?;\n        for template in templates {\n            let tests = &template.template.tests;\n            for (name, test) in tests {\n                if test.can_run_on(current_system) {\n                    println!(\n                        \"🧪 [{}#{}] Running test: {}\",\n                        url, template.template_name, name\n                    );\n                    test.run_test(\n                        &url.with_attr(&format!(\"{}.{}\", template.template_name, name)),\n                        &template,\n                    )\n                    .await?;\n                } else {\n                    println!(\n                        \"⚠️ Skipping test: {} (cannot run on {})\",\n                        name, current_system\n                    );\n                }\n            }\n        }\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "crates/omnix-cli/tests/command/mod.rs",
    "content": "mod ci;\nmod core;\nmod health;\nmod init;\nmod show;\n"
  },
  {
    "path": "crates/omnix-cli/tests/command/show.rs",
    "content": "use predicates::{prelude::*, str::contains};\n\nuse super::core::om;\n\n/// `om show` runs, and succeeds for a local flake.\n#[test]\nfn om_show_local() -> anyhow::Result<()> {\n    om()?.arg(\"show\").arg(\"../..\").assert().success().stdout(\n        contains(\"Packages\")\n            .and(contains(\"Apps\"))\n            .and(contains(\"Devshells\"))\n            .and(contains(\"Checks\")),\n    );\n    Ok(())\n}\n\n/// `om show` runs, and succeeds for a remote flake.\n#[test]\nfn om_show_remote() -> anyhow::Result<()> {\n    om()?\n        .arg(\"show\")\n        .arg(\"github:srid/haskell-multi-nix/c85563721c388629fa9e538a1d97274861bc8321\")\n        .assert()\n        .success()\n        .stdout(contains(\"bar\").and(contains(\n            \"github:srid/haskell-multi-nix/c85563721c388629fa9e538a1d97274861bc8321\",\n        )));\n    Ok(())\n}\n\n/// `om show` displays `nixosConfigurations`\n/// Note: This is used to test `evalOnAllSystems` (see: https://github.com/juspay/omnix/pull/277#discussion_r1760164052).\n#[test]\nfn om_show_nixos_configurations() -> anyhow::Result<()> {\n    om()?\n        .arg(\"show\")\n        .arg(\"github:srid/nixos-config/fe9c16cc6a60bbc17646c15c8ce3c5380239ab92\")\n        .assert()\n        .success()\n        .stdout(contains(\"NixOS Configurations\").and(contains(\"immediacy\")));\n    Ok(())\n}\n"
  },
  {
    "path": "crates/omnix-cli/tests/flake.nix",
    "content": "# A dummy flake to cache test dependencies in Nix store.\n{\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixpkgs-unstable\";\n    flake-parts.url = \"github:hercules-ci/flake-parts\";\n    systems.url = \"github:nix-systems/default\";\n\n    # NOTE: These inputs should kept in sync with those used in the Rust source (cli.rs)\n    haskell-multi-nix.url = \"github:srid/haskell-multi-nix/c85563721c388629fa9e538a1d97274861bc8321\";\n    services-flake.url = \"github:juspay/services-flake/23cf162387af041035072ee4a9de20f8408907cb\";\n    nixos-config.url = \"github:srid/nixos-config/fe9c16cc6a60bbc17646c15c8ce3c5380239ab92\";\n\n    # FIXME: Sadly, these will still result in rate-limiting errors, due to the 60/hour limit.\n    # See https://github.com/NixOS/nix/issues/5409\n\n    # system_list.rs tests\n    nix-systems-empty.url = \"github:nix-systems/empty\";\n    # Used in `om init` tests\n    haskell-flake.url = \"github:srid/haskell-flake\";\n    haskell-template.url = \"github:srid/haskell-template\";\n    rust-nix-template.url = \"github:srid/rust-nix-template\";\n    nixos-unified-template.url = \"github:juspay/nixos-unified-template\";\n  };\n\n  outputs = inputs:\n    inputs.flake-parts.lib.mkFlake { inherit inputs; } {\n      systems = import inputs.systems;\n      perSystem = { system, ... }: {\n        packages = {\n          haskell-multi-nix = inputs.haskell-multi-nix.packages.${system}.default;\n        };\n      };\n    };\n}\n"
  },
  {
    "path": "crates/omnix-cli/tests/test.rs",
    "content": "extern crate assert_matches;\nmod command;\n"
  },
  {
    "path": "crates/omnix-common/Cargo.toml",
    "content": "[package]\nname = \"omnix-common\"\nversion = \"1.3.0\"\nedition = \"2021\"\nrepository = \"https://github.com/juspay/omnix\"\nlicense = \"AGPL-3.0-only\"\ndescription = \"Common functionality for omnix frontends\"\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n[dependencies]\nanyhow = { workspace = true }\nasync-walkdir = { workspace = true }\nclap = { workspace = true }\nclap-verbosity-flag = { workspace = true }\nfutures-lite = { workspace = true }\nnix_rs = { workspace = true }\npulldown-cmark-mdcat = { workspace = true }\npulldown-cmark = { workspace = true }\nlazy_static = { workspace = true }\nserde = { workspace = true }\nsyntect = { workspace = true }\nthiserror = { workspace = true }\ntokio = { workspace = true }\ntracing = { workspace = true }\ntracing-subscriber = { workspace = true }\nwhich = { workspace = true }\nserde_json = { workspace = true }\nserde_yaml = { workspace = true }\n"
  },
  {
    "path": "crates/omnix-common/crate.nix",
    "content": "{\n  autoWire = [ ];\n}\n"
  },
  {
    "path": "crates/omnix-common/src/check.rs",
    "content": "//! Prerequisite checks for the Omnix project.\n\nuse std::path::PathBuf;\nuse which::{which, Error};\n\n/// Check if Nix is installed.\npub fn nix_installed() -> bool {\n    which_strict(\"nix\").is_some()\n}\n\n/// Check if a binary is available in the system's PATH and return its path.\n/// Returns None if the binary is not found, panics on unexpected errors.\npub fn which_strict(binary: &str) -> Option<PathBuf> {\n    match which(binary) {\n        Ok(path) => Some(path),\n        Err(Error::CannotFindBinaryPath) => None,\n        Err(e) => panic!(\n            \"Unexpected error while searching for binary '{}': {:?}\",\n            binary, e\n        ),\n    }\n}\n"
  },
  {
    "path": "crates/omnix-common/src/config.rs",
    "content": "//! Manage omnix configuration in flake.nix\n\nuse std::collections::BTreeMap;\n\nuse nix_rs::{\n    command::NixCmd,\n    flake::{command::FlakeOptions, eval::nix_eval_maybe, url::FlakeUrl},\n};\nuse serde::{de::DeserializeOwned, Deserialize};\n#[cfg(test)]\nuse std::str::FromStr;\n\n/// [OmConfigTree] with additional metadata about the flake URL and reference.\n///\n/// `reference` here is the part of the flake URL after `#`\n#[derive(Debug)]\npub struct OmConfig {\n    /// The flake URL used to load this configuration\n    pub flake_url: FlakeUrl,\n\n    /// The (nested) key reference into the flake config.\n    pub reference: Vec<String>,\n\n    /// The config tree\n    pub config: OmConfigTree,\n}\n\nimpl OmConfig {\n    /// Fetch the `om` configuration from `om.yaml` if present, falling back to `om` config in flake output\n    pub async fn get(nixcmd: &NixCmd, flake_url: &FlakeUrl) -> Result<Self, OmConfigError> {\n        match Self::from_yaml(nixcmd, flake_url).await? {\n            None => Self::from_flake(nixcmd, flake_url).await,\n            Some(config) => Ok(config),\n        }\n    }\n\n    /// Read the configuration from `om.yaml` in flake root\n    async fn from_yaml(\n        nixcmd: &NixCmd,\n        flake_url: &FlakeUrl,\n    ) -> Result<Option<Self>, OmConfigError> {\n        let path = if let Some(local_path) = flake_url.without_attr().as_local_path() {\n            local_path.to_path_buf()\n        } else {\n            (flake_url.without_attr())\n                .as_local_path_or_fetch(nixcmd)\n                .await?\n        }\n        .join(\"om.yaml\");\n\n        if !path.exists() {\n            tracing::debug!(\"{:?} does not exist; evaluating flake\", path);\n            return Ok(None);\n        }\n\n        let yaml_str = std::fs::read_to_string(path)?;\n        let config: OmConfigTree = serde_yaml::from_str(&yaml_str)?;\n        Ok(Some(OmConfig {\n            flake_url: flake_url.without_attr(),\n            reference: flake_url.get_attr().as_list(),\n            config,\n        }))\n    }\n\n    /// Read the configuration from `om` flake output\n    async fn from_flake(nixcmd: &NixCmd, flake_url: &FlakeUrl) -> Result<Self, OmConfigError> {\n        Ok(OmConfig {\n            flake_url: flake_url.without_attr(),\n            reference: flake_url.get_attr().as_list(),\n            config: nix_eval_maybe(nixcmd, &FlakeOptions::default(), &flake_url.with_attr(\"om\"))\n                .await?\n                .unwrap_or_default(),\n        })\n    }\n\n    /// Get the user referenced (per `referenced`) sub-tree under the given root key.\n    ///\n    /// get_sub_config_under(\"ci\") will return `ci.default` (or Default instance if config is missing) without a reference. Otherwise, it will use the reference to find the correct sub-tree.\n    pub fn get_sub_config_under<T>(&self, root_key: &str) -> Result<(T, &[String]), OmConfigError>\n    where\n        T: Default + DeserializeOwned + Clone,\n    {\n        // Get the config map, returning default if it doesn't exist\n        let config = match self.config.get::<T>(root_key)? {\n            Some(res) => res,\n            None => return Ok((T::default(), &[])),\n        };\n\n        let default = \"default\".to_string();\n        let (k, rest) = self.reference.split_first().unwrap_or((&default, &[]));\n\n        let v: &T = config\n            .get(k)\n            .ok_or(OmConfigError::MissingConfigAttribute(k.to_string()))?;\n        Ok((v.clone(), rest))\n    }\n}\n\n/// Represents the whole configuration for `omnix` parsed from JSON\n#[derive(Debug, Default, Deserialize)]\npub struct OmConfigTree(BTreeMap<String, BTreeMap<String, serde_json::Value>>);\n\nimpl OmConfigTree {\n    /// Get all the configs of type `T` for a given sub-config\n    ///\n    /// Return None if key doesn't exist\n    pub fn get<T>(&self, key: &str) -> Result<Option<BTreeMap<String, T>>, serde_json::Error>\n    where\n        T: DeserializeOwned,\n    {\n        match self.0.get(key) {\n            Some(config) => {\n                let result: Result<BTreeMap<String, T>, _> = config\n                    .iter()\n                    .map(|(k, v)| serde_json::from_value(v.clone()).map(|value| (k.clone(), value)))\n                    .collect();\n                result.map(Some)\n            }\n            None => Ok(None),\n        }\n    }\n}\n\n/// Error type for OmConfig\n#[derive(thiserror::Error, Debug)]\npub enum OmConfigError {\n    /// Missing configuration attribute\n    #[error(\"Missing configuration attribute: {0}\")]\n    MissingConfigAttribute(String),\n\n    /// A [nix_rs::command::NixCmdError]\n    #[error(\"Nix command error: {0}\")]\n    NixCmdError(#[from] nix_rs::command::NixCmdError),\n\n    /// Flake function error\n    #[error(\"Flake function error: {0}\")]\n    FlakeFnError(#[from] nix_rs::flake::functions::core::Error),\n\n    /// Failed to parse JSON\n    #[error(\"Failed to decode (json error): {0}\")]\n    DecodeErrorJson(#[from] serde_json::Error),\n\n    /// Failed to parse yaml\n    #[error(\"Failed to parse yaml: {0}\")]\n    ParseYaml(#[from] serde_yaml::Error),\n\n    /// Failed to read yaml\n    #[error(\"Failed to read yaml: {0}\")]\n    ReadYaml(#[from] std::io::Error),\n}\n\n#[tokio::test]\nasync fn test_get_missing_sub_config() {\n    let om_config_empty_reference = OmConfig {\n        flake_url: FlakeUrl::from_str(\".\").unwrap(),\n        reference: vec![],\n        config: serde_yaml::from_str(\"\").unwrap(),\n    };\n    let om_config_with_reference = OmConfig {\n        flake_url: FlakeUrl::from_str(\".\").unwrap(),\n        reference: vec![\"foo\".to_string()],\n        config: serde_yaml::from_str(\"\").unwrap(),\n    };\n\n    let (res_empty_reference, _rest) = om_config_empty_reference\n        .get_sub_config_under::<String>(\"health\")\n        .unwrap();\n    let (res_with_reference, _rest) = om_config_with_reference\n        .get_sub_config_under::<String>(\"health\")\n        .unwrap();\n\n    assert_eq!(res_empty_reference, String::default());\n    assert_eq!(res_with_reference, String::default());\n}\n\n#[tokio::test]\nasync fn test_get_omconfig_from_remote_flake_with_attr() {\n    let om_config = OmConfig::get(\n        NixCmd::get().await,\n        &FlakeUrl::from_str(\n            \"github:juspay/omnix/0ed2a389d6b4c8eb78caed778e20e872d2a59973#default.omnix\",\n        )\n        .unwrap(),\n    )\n    .await;\n    assert!(om_config.is_ok());\n}\n"
  },
  {
    "path": "crates/omnix-common/src/fs.rs",
    "content": "//! Filesystem utilities\nuse async_walkdir::{DirEntry, WalkDir};\nuse futures_lite::stream::StreamExt;\nuse std::{\n    os::unix::fs::PermissionsExt,\n    path::{Path, PathBuf},\n};\nuse tokio::fs;\n\n/// Copy a directory recursively\n///\n/// The target directory will always be user readable & writable.\npub async fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> anyhow::Result<()> {\n    let mut walker = WalkDir::new(&src);\n\n    while let Some(entry) = walker.next().await {\n        copy_entry(&src, entry?, &dst).await?;\n    }\n    Ok(())\n}\n\nasync fn copy_entry(\n    src: impl AsRef<Path>,\n    entry: DirEntry,\n    dst: impl AsRef<Path>,\n) -> anyhow::Result<()> {\n    let path = &entry.path();\n    let relative = path.strip_prefix(src)?;\n    let target = dst.as_ref().join(relative);\n\n    let file_type = entry.file_type().await?;\n    if file_type.is_dir() {\n        // Handle directories\n        fs::create_dir_all(&target).await?;\n    } else {\n        if let Some(parent) = target.parent() {\n            fs::create_dir_all(parent).await?;\n        }\n        if file_type.is_symlink() {\n            // Handle symlinks *as is* (we expect relative symlink targets) without resolution.\n            let link_target = fs::read_link(path).await?;\n            fs::symlink(&link_target, &target).await?;\n        } else {\n            // Handle regular files\n            fs::copy(path, &target).await?;\n            // Because we are copying from the Nix store, the source paths will be read-only.\n            // So, make the target writeable by the owner.\n            make_owner_writeable(&target).await?;\n        }\n    }\n    Ok(())\n}\n\nasync fn make_owner_writeable(path: impl AsRef<Path>) -> anyhow::Result<()> {\n    let path = path.as_ref();\n    let mut perms = fs::metadata(path).await?.permissions();\n    perms.set_mode(perms.mode() | 0o600); // Read/write for owner\n    fs::set_permissions(path, perms).await?;\n    Ok(())\n}\n\n/// Recursively find paths under a directory\n///\n/// Returned list of files or directories are relative to the given directory.\npub async fn find_paths(dir: impl AsRef<Path> + Copy) -> anyhow::Result<Vec<PathBuf>> {\n    let mut paths = Vec::new();\n    let mut walker = WalkDir::new(dir);\n\n    while let Some(entry) = walker.next().await {\n        let entry = entry?;\n        let path = entry.path();\n        paths.push(path.strip_prefix(dir)?.to_path_buf());\n    }\n\n    Ok(paths)\n}\n\n/// Recursively delete the path\npub async fn remove_all(path: impl AsRef<Path>) -> anyhow::Result<()> {\n    let path = path.as_ref();\n    if path.is_dir() {\n        fs::remove_dir_all(path).await?;\n    } else {\n        fs::remove_file(path).await?;\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "crates/omnix-common/src/lib.rs",
    "content": "//! Omnix library crate\n#![warn(missing_docs)]\npub mod check;\npub mod config;\npub mod fs;\npub mod logging;\npub mod markdown;\n"
  },
  {
    "path": "crates/omnix-common/src/logging.rs",
    "content": "//! Logging setup for omnix\n\nuse clap_verbosity_flag::{InfoLevel, Level, Verbosity};\nuse std::fmt;\nuse tracing::{Event, Subscriber};\nuse tracing_subscriber::{\n    filter::{Directive, LevelFilter},\n    fmt::{format, FmtContext, FormatEvent, FormatFields},\n    registry::LookupSpan,\n    EnvFilter,\n};\n\n/// Setup logging for whole application\npub fn setup_logging(verbosity: &Verbosity<InfoLevel>, bare: bool) {\n    let builder = tracing_subscriber::fmt()\n        .with_writer(std::io::stderr)\n        .with_env_filter(log_filter(verbosity))\n        .compact();\n    if bare {\n        builder.event_format(BareFormatter).init();\n    } else {\n        builder.init();\n    }\n}\n\n/// Return the log filter for CLI flag.\nfn log_filter(v: &Verbosity<InfoLevel>) -> EnvFilter {\n    log_directives(v)\n        .iter()\n        .fold(EnvFilter::from_env(\"OMNIX_LOG\"), |filter, directive| {\n            filter.add_directive(directive.clone())\n        })\n}\n\nfn log_directives(v: &Verbosity<InfoLevel>) -> Vec<Directive> {\n    // Allow warnings+errors from all crates.\n    match v.log_level() {\n        None => vec![LevelFilter::WARN.into()],\n        Some(Level::Warn) => vec![LevelFilter::WARN.into()],\n        Some(Level::Error) => vec![LevelFilter::ERROR.into()],\n        // Default\n        Some(Level::Info) => vec![\n            LevelFilter::WARN.into(),\n            \"om=info\".parse().unwrap(),\n            \"nix_rs=info\".parse().unwrap(),\n            \"omnix-health=info\".parse().unwrap(),\n            \"omnix-ci=info\".parse().unwrap(),\n            \"omnix-init=info\".parse().unwrap(),\n            \"omnix-hack=info\".parse().unwrap(),\n        ],\n        // -v: log app DEBUG level, as well as http requests\n        Some(Level::Debug) => vec![\n            LevelFilter::WARN.into(),\n            \"om=debug\".parse().unwrap(),\n            \"nix_rs=debug\".parse().unwrap(),\n            \"omnix-health=debug\".parse().unwrap(),\n            \"omnix-ci=debug\".parse().unwrap(),\n            \"omnix-init=debug\".parse().unwrap(),\n            \"omnix-hack=debug\".parse().unwrap(),\n        ],\n        // -vv: log app TRACE level, as well as http requests\n        Some(Level::Trace) => vec![\n            LevelFilter::WARN.into(),\n            \"om=trace\".parse().unwrap(),\n            \"nix_rs=trace\".parse().unwrap(),\n            \"omnix-health=trace\".parse().unwrap(),\n            \"omnix-ci=trace\".parse().unwrap(),\n            \"omnix-init=trace\".parse().unwrap(),\n            \"omnix-hack=trace\".parse().unwrap(),\n        ],\n        // -vvv: log DEBUG level of app and libraries\n        // 3 => vec![LevelFilter::DEBUG.into()],\n        // -vvvv: log TRACE level of app and libraries\n        // _ => vec![LevelFilter::TRACE.into()],\n    }\n}\n\n/// A [tracing_subscriber] event formatter that suppresses everything but the\n/// log message.\nstruct BareFormatter;\n\nimpl<S, N> FormatEvent<S, N> for BareFormatter\nwhere\n    S: Subscriber + for<'a> LookupSpan<'a>,\n    N: for<'a> FormatFields<'a> + 'static,\n{\n    fn format_event(\n        &self,\n        ctx: &FmtContext<'_, S, N>,\n        mut writer: format::Writer<'_>,\n        event: &Event<'_>,\n    ) -> fmt::Result {\n        /*\n        let metadata = event.metadata();\n        if metadata.level() != &tracing::Level::INFO {\n            write!(&mut writer, \"{} {}: \", metadata.level(), metadata.target())?;\n        }\n        */\n        ctx.field_format().format_fields(writer.by_ref(), event)?;\n        writeln!(writer)\n    }\n}\n"
  },
  {
    "path": "crates/omnix-common/src/markdown.rs",
    "content": "//! Markdown rendering using `mdcat`\nuse anyhow::Context;\nuse lazy_static::lazy_static;\nuse pulldown_cmark::{Options, Parser};\nuse pulldown_cmark_mdcat::{\n    resources::FileResourceHandler, Environment, Settings, TerminalProgram, TerminalSize, Theme,\n};\nuse std::{io::Write, path::Path};\nuse syntect::parsing::SyntaxSet;\n\nlazy_static! {\n    static ref SYNTAX_SET: SyntaxSet = SyntaxSet::load_defaults_newlines();\n\n    /// Global settings for rendering markdown\n    pub static ref SETTINGS: Settings<'static> = Settings {\n        terminal_capabilities: TerminalProgram::detect().capabilities(),\n        terminal_size: TerminalSize::from_terminal().unwrap_or_default(),\n        theme: Theme::default(),\n        syntax_set: &SYNTAX_SET,\n    };\n}\n\n/// Print Markdown using `mdcat` to STDERR\npub async fn print_markdown(base_dir: &Path, s: &str) -> anyhow::Result<()> {\n    print_markdown_to(base_dir, &mut std::io::stderr(), s)\n        .await\n        .with_context(|| \"Cannot print markdown\")\n}\n\n/// Render Markdown into a string to be printed to terminal\npub async fn render_markdown(base_dir: &Path, s: &str) -> anyhow::Result<String> {\n    let mut w = Vec::new();\n    print_markdown_to(base_dir, &mut w, s)\n        .await\n        .with_context(|| \"Cannot render markdown\")?;\n    let s = String::from_utf8(w)?;\n    // A trim is needed to remove unnecessary newlines at end (which can impact for single-line renders)\n    Ok(s.trim().to_string())\n}\n\nasync fn print_markdown_to<W>(base_dir: &Path, w: &mut W, s: &str) -> anyhow::Result<()>\nwhere\n    W: Write,\n{\n    let env = Environment::for_local_directory(&base_dir)?;\n    let handler = FileResourceHandler::new(200000);\n    let parser = Parser::new_ext(\n        s,\n        Options::ENABLE_TASKLISTS\n            | Options::ENABLE_STRIKETHROUGH\n            | Options::ENABLE_TABLES\n            | Options::ENABLE_GFM,\n    );\n\n    pulldown_cmark_mdcat::push_tty(&SETTINGS, &env, &handler, w, parser)?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "crates/omnix-develop/Cargo.toml",
    "content": "[package]\nauthors = [\"Sridhar Ratnakumar <srid@srid.ca>\"]\nedition = \"2021\"\n# If you change the name here, you must also do it in flake.nix (and run `cargo generate-lockfile` afterwards)\nname = \"omnix-develop\"\nversion = \"1.3.2\"\nrepository = \"https://github.com/juspay/omnix\"\ndescription = \"Implementation for the `om develop` command\"\nlicense = \"Apache-2.0\"\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nanyhow = { workspace = true }\nlazy_static = { workspace = true }\nnix_rs = { workspace = true }\nserde = { workspace = true }\nserde_json = { workspace = true }\nsyntect = { workspace = true }\nthiserror = { workspace = true }\ntokio = { workspace = true }\ntracing = { workspace = true }\ntracing-subscriber = { workspace = true }\nomnix-common = { workspace = true }\nomnix-health = { workspace = true }\n"
  },
  {
    "path": "crates/omnix-develop/crate.nix",
    "content": "{\n  autoWire = [ ];\n  crane.args = { };\n}\n"
  },
  {
    "path": "crates/omnix-develop/src/config.rs",
    "content": "use serde::Deserialize;\n\nuse omnix_common::config::OmConfig;\n\nuse crate::readme::Readme;\n\n#[derive(Debug, Deserialize, Clone, Default)]\npub struct DevelopConfig {\n    pub readme: Readme,\n}\n\n#[derive(Debug, Deserialize, Clone)]\npub struct CacheConfig {\n    /// Cache substituter URL\n    pub url: String,\n}\n\nimpl DevelopConfig {\n    pub fn from_om_config(om_config: &OmConfig) -> anyhow::Result<Self> {\n        let (config, _rest) = om_config.get_sub_config_under(\"develop\")?;\n        Ok(config)\n    }\n}\n"
  },
  {
    "path": "crates/omnix-develop/src/core.rs",
    "content": "use anyhow::Context;\nuse std::{env::current_dir, path::PathBuf};\n\nuse nix_rs::{flake::url::FlakeUrl, info::NixInfo};\nuse omnix_common::{config::OmConfig, markdown::print_markdown};\nuse omnix_health::{check::caches::CachixCache, traits::Checkable, NixHealth};\n\nuse crate::config::DevelopConfig;\n\n/// A project that an be developed on locally.\npub struct Project {\n    /// The directory of the project.\n    pub dir: Option<PathBuf>,\n    /// [FlakeUrl] corresponding to the project.\n    pub flake: FlakeUrl,\n    /// The `om` configuration\n    pub om_config: OmConfig,\n}\n\nimpl Project {\n    pub async fn new(flake: FlakeUrl, om_config: OmConfig) -> anyhow::Result<Self> {\n        let dir = match flake.as_local_path() {\n            Some(path) => Some(path.canonicalize()?),\n            None => None,\n        };\n        Ok(Self {\n            dir,\n            flake,\n            om_config,\n        })\n    }\n}\n\npub async fn develop_on(prj: &Project) -> anyhow::Result<()> {\n    develop_on_pre_shell(prj).await?;\n    develop_on_post_shell(prj).await?;\n\n    tracing::warn!(\"\");\n    tracing::warn!(\"🚧 !!!!\");\n    tracing::warn!(\"🚧 Not invoking Nix devShell (not supported yet). Please use `direnv`!\");\n    tracing::warn!(\"🚧 !!!!\");\n    Ok(())\n}\n\npub async fn develop_on_pre_shell(prj: &Project) -> anyhow::Result<()> {\n    // Run relevant `om health` checks\n    let health = NixHealth::from_om_config(&prj.om_config)?;\n    let nix_info = NixInfo::get()\n        .await\n        .as_ref()\n        .with_context(|| \"Unable to gather nix info\")?;\n\n    let mut relevant_checks: Vec<&'_ dyn Checkable> =\n        vec![&health.nix_version, &health.rosetta, &health.max_jobs];\n    if !health.caches.required.is_empty() {\n        relevant_checks.push(&health.trusted_users);\n    };\n\n    // Run cache related checks, and try to resolve it automatically using `cachix use` as appropriate\n    if !health.caches.required.is_empty() {\n        let missing = health.caches.get_missing_caches(nix_info);\n        let (missing_cachix, missing_other) = parse_many(&missing, CachixCache::from_url);\n        for cachix_cache in &missing_cachix {\n            tracing::info!(\"🐦 Running `cachix use` for {}\", cachix_cache.0);\n            cachix_cache.cachix_use().await?;\n        }\n        if !missing_other.is_empty() {\n            // We cannot add these caches automatically, so defer to `om health`\n            relevant_checks.push(&health.caches);\n        };\n        // TODO: Re-calculate NixInfo since our nix.conf has changed (due to `cachix use`)\n        // To better implement this, we need a mutable database of NixInfo, NixConfig, etc. OnceCell is not sufficient\n    };\n\n    for check_kind in relevant_checks.into_iter() {\n        for (_, check) in check_kind.check(nix_info, Some(&prj.flake)) {\n            if !check.result.green() {\n                check.tracing_log().await?;\n                if !check.result.green() && check.required {\n                    anyhow::bail!(\"ERROR: Your Nix environment is not properly setup. See suggestions above, or run `om health` for details.\");\n                };\n            };\n        }\n    }\n\n    tracing::info!(\"✅ Nix environment is healthy.\");\n\n    Ok(())\n}\n\npub async fn develop_on_post_shell(prj: &Project) -> anyhow::Result<()> {\n    eprintln!();\n    let pwd = current_dir()?;\n    let dir = prj.dir.as_ref().unwrap_or(&pwd);\n    let cfg = DevelopConfig::from_om_config(&prj.om_config)?;\n    print_markdown(dir, cfg.readme.get_markdown()).await?;\n    Ok(())\n}\n\n/// Parse all items using the given parse function\nfn parse_many<'a, T, Q, F>(vec: &'a [T], f: F) -> (Vec<Q>, Vec<&'a T>)\nwhere\n    F: Fn(&T) -> Option<Q>,\n{\n    let mut successes: Vec<Q> = Vec::new();\n    let mut failures: Vec<&'a T> = Vec::new();\n\n    for item in vec {\n        match f(item) {\n            Some(transformed) => successes.push(transformed),\n            None => failures.push(item),\n        }\n    }\n\n    (successes, failures)\n}\n"
  },
  {
    "path": "crates/omnix-develop/src/lib.rs",
    "content": "pub mod config;\npub mod core;\npub mod readme;\n"
  },
  {
    "path": "crates/omnix-develop/src/readme.rs",
    "content": "use serde::Deserialize;\n\nconst DEFAULT: &str = r#\"🍾 Welcome to the project\n\n*(Want to show custom instructions here? Add them to the `om.develop.default.readme` field in your `flake.nix` or `om.yaml` file)*\n\"#;\n\n/// The README to display at the end.\n#[derive(Debug, Deserialize, Clone)]\npub struct Readme(pub String);\n\nimpl Default for Readme {\n    fn default() -> Self {\n        Self(DEFAULT.to_string())\n    }\n}\n\nimpl Readme {\n    /// Get the Markdown string\n    pub fn get_markdown(&self) -> &str {\n        &self.0\n    }\n}\n"
  },
  {
    "path": "crates/omnix-gui/Cargo.toml",
    "content": "[package]\nedition = \"2021\"\nlicense = \"AGPL-3.0-only\"\nrepository = \"https://github.com/juspay/omnix\"\n# If you change the name here, you must also do it in flake.nix (and run `cargo generate-lockfile` afterwards)\nname = \"omnix-gui\"\nversion = \"0.1.0\"\nhomepage = \"https://github.com/juspay/omnix\"\nbuild = \"build.rs\"\ndescription = \"Graphical interface for Omnix\"\n\n[dependencies]\nanyhow = { workspace = true }\nbytesize = { workspace = true }\ncfg-if = { workspace = true }\nclap = { workspace = true }\nclap-verbosity-flag = { workspace = true }\nconsole_error_panic_hook = { workspace = true }\nconsole_log = { workspace = true }\ndioxus = { version = \"0.5.0\", features = [\"desktop\"] }\ndioxus-desktop = \"0.5.0\"\ndioxus-router = \"0.5.0\"\ndioxus-sdk = { version = \"0.5.0\", features = [\"storage\"] }\ndioxus-signals = \"0.5.0\"\ndirenv = { workspace = true }\nfermi = { workspace = true }\nhttp = { workspace = true }\nhuman-panic = { workspace = true }\nomnix-health = { workspace = true }\nnix_rs = { workspace = true }\nomnix-common = { workspace = true }\nregex = { workspace = true }\nserde = { workspace = true }\nserde_json = { workspace = true }\nserde_with = { workspace = true }\nthiserror = { workspace = true }\ntokio = { workspace = true }\ntracing = { workspace = true }\nuuid = { workspace = true }\n\n[package.metadata.docs.rs]\nall-features = true\n"
  },
  {
    "path": "crates/omnix-gui/Dioxus.toml",
    "content": "[application]\nname = \"omnix-gui\"\ndefault_platform = \"desktop\"\nout_dir = \"dist\"\nasset_dir = \"assets\"\n\n[web.app]\ntitle = \"Omnix | 🌍\"\n\n[web.watcher]\n# when watcher trigger, regenerate the `index.html`\nreload_html = true\n# which files or dirs will be watcher monitoring\nwatch_path = [\"src\", \"assets\"]\n\n[web.resource]\n# CSS style file\nstyle = [\"tailwind.css\"]\n# Javascript code file\nscript = []\n\n[web.resource.dev]\n# CSS style file\nstyle = []\n# Javascript code file\nscript = []\n\n# FIXME: Need to `cd assets` before running `dx bundle` due to https://github.com/DioxusLabs/dioxus/issues/1283\n[bundle]\nname = \"Omnix\"\nidentifier = \"in.juspay.omnix\"\nicon = [\"images/128x128.png\"]  # [\"32x32.png\", \"128x128.png\", \"128x128@2x.png\"]\nversion = \"1.0.0\"\n# TODO: Must add these files\nresources = [\"**/tailwind.css\", \"images/**/*.png\"] # , \"secrets/public_key.txt\"]\ncopyright = \"Copyright (c) Juspay 2023. All rights reserved.\"\ncategory = \"Developer Tool\"\nshort_description = \"Graphical user interface for Omnix\"\nlong_description = \"\"\"\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do\neiusmod tempor incididunt ut labore et dolore magna aliqua.  Ut\nenim ad minim veniam, quis nostrud exercitation ullamco laboris\nnisi ut aliquip ex ea commodo consequat.\n\"\"\"\nosx_frameworks = []\n"
  },
  {
    "path": "crates/omnix-gui/assets/tailwind.css",
    "content": "/*! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:\"\"}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:Proxima Nova,ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.static{position:static}.m-2{margin:.5rem}.my-1{margin-bottom:.25rem;margin-top:.25rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.my-4{margin-bottom:1rem;margin-top:1rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mr-2{margin-right:.5rem}.mt-4{margin-top:1rem}.flex{display:flex}.table{display:table}.contents{display:contents}.hidden{display:none}.h-16{height:4rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-screen{height:100vh}.w-16{width:4rem}.w-6{width:1.5rem}.w-full{width:100%}.max-w-prose{max-width:65ch}.flex-1{flex:1 1 0%}.scale-x-\\[-1\\]{--tw-scale-x:-1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-auto{cursor:auto}.cursor-pointer{cursor:pointer}.list-disc{list-style-type:disc}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border-2{border-width:2px}.border-4{border-width:4px}.border-b-2{border-bottom-width:2px}.border-l-2{border-left-width:2px}.border-t-2{border-top-width:2px}.border-base-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-base-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.border-black{--tw-border-opacity:1;border-color:rgb(0 0 0/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-purple-500{--tw-border-opacity:1;border-color:rgb(168 85 247/var(--tw-border-opacity))}.bg-base-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-primary-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-5xl{font-size:3rem;line-height:1}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-light{font-weight:300}.font-semibold{font-weight:600}.italic{font-style:italic}.text-base-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.text-base-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-base-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.text-primary-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-primary-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\\:scale-125:hover{--tw-scale-x:1.25;--tw-scale-y:1.25;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\\:border-gray-400:hover{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.hover\\:bg-primary-100:hover{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.hover\\:text-primary-500:hover{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.hover\\:underline:hover{text-decoration-line:underline}.hover\\:no-underline:hover{text-decoration-line:none}.active\\:scale-100:active{--tw-scale-x:1;--tw-scale-y:1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}"
  },
  {
    "path": "crates/omnix-gui/build.rs",
    "content": "use std::process::Command;\n\nconst INPUT_CSS_PATH: &str = \"./css/input.css\";\nconst PUBLIC_DIR: &str = \"./assets/\";\n\nfn main() {\n    run_tailwind();\n}\n\nfn run_tailwind() {\n    let mut command = Command::new(\"tailwindcss\");\n\n    command\n        .args([\n            \"-i\",\n            INPUT_CSS_PATH,\n            \"-o\",\n            &(PUBLIC_DIR.to_string() + \"tailwind.css\"),\n            \"--minify\",\n        ])\n        .spawn()\n        .expect(\"couldn't run tailwind. Please run it manually\");\n}\n"
  },
  {
    "path": "crates/omnix-gui/crate.nix",
    "content": "{ flake\n, pkgs\n, lib\n, rust-project\n, ...\n}:\n\nlet\n  inherit (flake) inputs;\nin\n{\n  autoWire = [ ];\n  crane = {\n    args = {\n      buildInputs = lib.optionals pkgs.stdenv.isLinux\n        (with pkgs; [\n          webkitgtk_4_1\n          xdotool\n          pkg-config\n        ]);\n      nativeBuildInputs = with pkgs;[\n        pkg-config\n        makeWrapper\n        tailwindcss\n        dioxus-cli\n        # pkgs.nix # cargo tests need nix\n      ];\n      meta.description = \"Graphical user interface for Omnix\";\n    };\n  };\n}\n"
  },
  {
    "path": "crates/omnix-gui/css/input.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "crates/omnix-gui/src/app/flake.rs",
    "content": "//! UI for /flake segment of the app\n\nuse std::{collections::BTreeMap, path::PathBuf};\n\nuse dioxus::prelude::*;\nuse dioxus_router::components::Link;\nuse nix_rs::flake::{\n    outputs::{FlakeOutputs, Leaf, Type, Val},\n    schema::FlakeSchema,\n    url::FlakeUrl,\n    Flake,\n};\n\nuse crate::{\n    app::widget::FolderDialogButton,\n    app::{state::AppState, widget::Loader, Route},\n};\n\n#[component]\npub fn Flake() -> Element {\n    let state = AppState::use_state();\n    let flake = state.flake.read();\n    rsx! {\n        h1 { class: \"text-5xl font-bold\", \"Flake browser\" }\n        { FlakeInput() },\n        if flake.is_loading_or_refreshing() {\n            Loader {}\n        }\n        { flake.render_with(|v| rsx! { FlakeView { flake: v.clone() } }) }\n    }\n}\n\n#[component]\npub fn FlakeInput() -> Element {\n    let state = AppState::use_state();\n    let busy = state.flake.read().is_loading_or_refreshing();\n    rsx! {\n        div { class: \"p-2 my-1 flex w-full\",\n            input {\n                class: \"flex-1 w-full p-1 mb-4 font-mono\",\n                id: \"nix-flake-input\",\n                \"type\": \"text\",\n                value: state.get_flake_url_string(),\n                disabled: busy,\n                onchange: move |ev| {\n                    let url: FlakeUrl = str::parse(&ev.value()).unwrap();\n                    Route::go_to_flake(url);\n                }\n            }\n            div { class: \"ml-2 flex flex-col\",\n                { FolderDialogButton(\n                    move |flake_path: PathBuf| {\n                        let url: FlakeUrl = flake_path.into();\n                        Route::go_to_flake(url);\n                    }\n                ) }\n            }\n        }\n    }\n}\n\n#[component]\npub fn FlakeRaw() -> Element {\n    let state = AppState::use_state();\n    // use_future(cx, (), |_| async move { state.update_flake().await });\n    let flake = state.flake.read();\n    rsx! {\n        div {\n            Link { to: Route::Flake {}, \"⬅ Back\" }\n            div { class: \"px-4 py-2 font-mono text-xs text-left text-gray-500 border-2 border-black\",\n                { flake.render_with(|v| rsx! { FlakeOutputsRawView { outs: v.output.clone() } } ) }\n            }\n        }\n    }\n}\n\n#[component]\npub fn FlakeView(flake: Flake) -> Element {\n    rsx! {\n        div { class: \"flex flex-col my-4\",\n            h3 { class: \"text-lg font-bold\", { flake.url.to_string() } }\n            div { class: \"text-sm italic text-gray-600\",\n                Link { to: Route::FlakeRaw {}, \"View raw output\" }\n            }\n            FlakeSchemaView { schema: flake.schema }\n        }\n    }\n}\n\n#[component]\npub fn SectionHeading(title: &'static str, extra: Option<String>) -> Element {\n    rsx! {\n        h3 { class: \"p-2 mt-4 mb-2 font-bold bg-gray-300 border-b-2 border-l-2 border-black text-l\",\n            \"{title}\"\n            match extra {\n                Some(v) => rsx! { span { class: \"text-xs text-gray-500 ml-1\", \"(\", \"{v}\", \")\" } },\n                None => rsx! { \"\" }\n            }\n        }\n    }\n}\n\n#[component]\npub fn FlakeSchemaView(schema: FlakeSchema) -> Element {\n    let system = schema.system.clone();\n    rsx! {\n        div {\n            h2 { class: \"my-2\",\n                div { class: \"text-xl font-bold text-primary-600\", \"{system.human_readable()}\" }\n                span { class: \"font-mono text-xs text-gray-500\",\n                    \"(\"\n                    \"{system }\"\n                    \")\"\n                }\n            }\n            div { class: \"text-left\",\n                BtreeMapView { title: \"Packages\", tree: schema.packages }\n                BtreeMapView { title: \"Legacy Packages\", tree: schema.legacy_packages }\n                BtreeMapView { title: \"Dev Shells\", tree: schema.devshells }\n                BtreeMapView { title: \"Checks\", tree: schema.checks }\n                BtreeMapView { title: \"Apps\", tree: schema.apps }\n                BtreeMapView { title: \"NixOS configurations\", tree: schema.nixos_configurations }\n                BtreeMapView { title: \"Darwin configurations\", tree: schema.darwin_configurations }\n                BtreeMapView { title: \"NixOS modules\", tree: schema.nixos_modules }\n                SectionHeading { title: \"Formatter\" }\n                match schema.formatter.as_ref() {\n                    Some(l) => {\n                        let v = l.as_val().cloned().unwrap_or_default();\n                        let k = v.derivation_name.as_deref().unwrap_or(\"formatter\");\n                        rsx! { FlakeValView { k: k, v: v.clone() } }\n                    },\n                    None => rsx! { \"\" }\n                },\n                SectionHeading { title: \"Other\" }\n                match &schema.other {\n                    Some(v) => rsx! { FlakeOutputsRawView { outs: FlakeOutputs::Attrset(v.clone()) } },\n                    None => rsx! { \"\" }\n                }\n            }\n        }\n    }\n}\n\n#[component]\npub fn BtreeMapView(title: &'static str, tree: BTreeMap<String, Leaf>) -> Element {\n    rsx! {\n        div {\n            SectionHeading { title, extra: tree.len().to_string() }\n            BtreeMapBodyView { tree }\n        }\n    }\n}\n\n#[component]\npub fn BtreeMapBodyView(tree: BTreeMap<String, Leaf>) -> Element {\n    rsx! {\n        div { class: \"flex flex-wrap justify-start\",\n            for (k , l) in tree.iter() {\n                FlakeValView { k: k.clone(), v: l.as_val().cloned().unwrap_or_default() }\n            }\n        }\n    }\n}\n\n#[component]\npub fn FlakeValView(k: String, v: Val) -> Element {\n    rsx! {\n        div {\n            title: \"{v.type_}\",\n            class: \"flex flex-col p-2 my-2 mr-2 space-y-2 bg-white border-4 border-gray-300 rounded hover:border-gray-400\",\n            div { class: \"flex flex-row justify-start space-x-2 font-bold text-primary-500\",\n                div { { v.type_.to_icon() } }\n                div { \"{k}\" }\n            }\n            match &v.derivation_name {\n                Some(name_val) => rsx! { div { class: \"font-mono text-xs text-gray-500\", \"{name_val}\" } },\n                None => rsx! { \"\" } // No-op for None\n            },\n            match &v.short_description {\n                Some(desc_val) => rsx! { div { class: \"font-light\", \"{desc_val}\" } },\n                None => rsx! { \"\" } // No-op for None\n            }\n        }\n    }\n}\n\n/// This component renders recursively. This view is used to see the raw flake\n/// output only; it is not useful for general UX.\n///\n/// WARNING: This may cause performance problems if the tree is large.\n#[component]\npub fn FlakeOutputsRawView(outs: FlakeOutputs) -> Element {\n    #[component]\n    fn ValView(val: Val) -> Element {\n        rsx! {\n            span {\n                b { { val.derivation_name.clone()  } }\n                \" (\"\n                TypeView { type_: val.type_ }\n                \") \"\n                em { { val.short_description.clone() } }\n            }\n        }\n    }\n\n    #[component]\n    pub fn TypeView(type_: Type) -> Element {\n        rsx! {\n            span {\n                match type_ {\n                    Type::NixosModule => \"nixosModule ❄️\",\n                    Type::NixosConfiguration => \"nixosConfiguration 🧩\",\n                    Type::DarwinConfiguration => \"darwinConfiguration 🍏\",\n                    Type::Package => \"package 📦\",\n                    Type::DevShell => \"devShell 🐚\",\n                    Type::Check => \"check 🧪\",\n                    Type::App => \"app 📱\",\n                    Type::Template => \"template 🏗️\",\n                    Type::Unknown => \"unknown ❓\",\n                }\n            }\n        }\n    }\n\n    match outs {\n        FlakeOutputs::Leaf(l) => rsx! {\n            ValView { val: l.as_val().cloned().unwrap_or_default() }\n        },\n        FlakeOutputs::Attrset(v) => rsx! {\n            ul { class: \"list-disc\",\n                for (k , v) in v.iter() {\n                    li { class: \"ml-4\",\n                        span { class: \"px-2 py-1 font-bold text-primary-500\", \"{k}\" }\n                        FlakeOutputsRawView { outs: v.clone() }\n                    }\n                }\n            }\n        },\n    }\n}\n"
  },
  {
    "path": "crates/omnix-gui/src/app/health.rs",
    "content": "//! Nix health check UI\n\nuse dioxus::prelude::*;\nuse omnix_health::traits::{Check, CheckResult};\n\nuse crate::{app::state::AppState, app::widget::Loader};\n\n/// Nix health checks\npub fn Health() -> Element {\n    let state = AppState::use_state();\n    let health_checks = state.health_checks.read();\n    let title = \"Nix Health\";\n    rsx! {\n        h1 { class: \"text-5xl font-bold\", title }\n        if health_checks.is_loading_or_refreshing() {\n            Loader {}\n        }\n        { health_checks.render_with(|checks| rsx! {\n            div { class: \"flex flex-col items-stretch justify-start space-y-8 text-left\",\n                for check in checks {\n                        ViewCheck { check: check.clone() }\n                }\n            }\n        }) }\n    }\n}\n\n#[component]\nfn ViewCheck(check: Check) -> Element {\n    rsx! {\n        div { class: \"contents\",\n            details {\n                open: check.result != CheckResult::Green,\n                class: \"my-2 bg-white border-2 rounded-lg cursor-pointer hover:bg-primary-100 border-base-300\",\n                summary { class: \"p-4 text-xl font-bold\",\n                    CheckResultSummaryView { green: check.result.green() }\n                    \" \"\n                    { check.title.clone() }\n                }\n                div { class: \"p-4\",\n                    div { class: \"p-2 my-2 font-mono text-sm bg-black text-base-100\",\n                        { check.info.clone() }\n                    }\n                    div { class: \"flex flex-col justify-start space-y-4\",\n                        match check.result.clone() {\n                            CheckResult::Green => rsx! { \"\" },\n                            CheckResult::Red { msg, suggestion } => rsx! {\n                                h3 { class: \"my-2 font-bold text-l\" }\n                                div { class: \"p-2 bg-red-400 rounded bg-border\", { msg } }\n                                h3 { class: \"my-2 font-bold text-l\" }\n                                div { class: \"p-2 bg-blue-400 rounded bg-border\", { suggestion } }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n#[component]\npub fn CheckResultSummaryView(green: bool) -> Element {\n    if green {\n        rsx! {\n            span { class: \"text-green-500\", \"✓\" }\n        }\n    } else {\n        rsx! {\n            span { class: \"text-red-500\", \"✗\" }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/omnix-gui/src/app/info.rs",
    "content": "//! Nix info UI\n\nuse std::fmt::Display;\n\nuse dioxus::prelude::*;\nuse nix_rs::{config::NixConfig, env::NixEnv, info::NixInfo, version::NixVersion};\n\nuse crate::{app::state::AppState, app::widget::Loader};\n\n/// Nix information\n#[component]\npub fn Info() -> Element {\n    let title = \"Nix Info\";\n    let state = AppState::use_state();\n    let nix_info = state.nix_info.read();\n    rsx! {\n        h1 { class: \"text-5xl font-bold\", title }\n        if nix_info.is_loading_or_refreshing() {\n            Loader {}\n        }\n        div { class: \"flex items-center justify-center\",\n            { nix_info.render_with(|v| rsx! { NixInfoView { info: v.clone() } }) }\n        }\n    }\n}\n\n#[component]\nfn NixInfoView(info: NixInfo) -> Element {\n    rsx! {\n        div { class: \"flex flex-col max-w-prose p-4 space-y-8 bg-white border-2 rounded border-base-400\",\n            div {\n                b { \"Nix Version\" }\n                div { class: \"p-1 my-1 rounded bg-primary-50\",\n                    NixVersionView { version: info.nix_version }\n                }\n            }\n            div {\n                b { \"Nix Config\" }\n                NixConfigView { config: info.nix_config.clone() }\n            }\n            div {\n                b { \"Nix Env\" }\n                NixEnvView { env: info.nix_env.clone() }\n            }\n        }\n    }\n}\n\n#[component]\nfn NixVersionView(version: NixVersion) -> Element {\n    rsx! {\n        a {\n            href: nix_rs::refs::RELEASE_HISTORY,\n            class: \"font-mono hover:underline\",\n            target: \"_blank\",\n            \"{version}\"\n        }\n    }\n}\n\n#[component]\nfn NixConfigView(config: NixConfig) -> Element {\n    rsx! {\n        div { class: \"py-1 my-1 rounded bg-primary-50\",\n            table { class: \"text-right\",\n                tbody {\n                    TableRow { name: \"Local System\", title: config.system.description, \"{config.system.value}\" }\n                    TableRow { name: \"Max Jobs\", title: config.max_jobs.description, \"{config.max_jobs.value}\" }\n                    TableRow { name: \"Cores per build\", title: config.cores.description, \"{config.cores.value}\" }\n                    TableRow { name: \"Nix Caches\", title: config.substituters.description,\n                        ConfigValList { items: config.substituters.value }\n                    }\n                }\n            }\n        }\n    }\n}\n\n#[component]\nfn ConfigValList<T: 'static + PartialEq + Display>(items: Vec<T>) -> Element {\n    rsx! {\n        div { class: \"flex flex-col space-y-4\",\n            for item in items {\n                li { class: \"list-disc\", \"{item}\" }\n            }\n        }\n    }\n}\n\n#[component]\nfn NixEnvView(env: NixEnv) -> Element {\n    rsx! {\n        div { class: \"py-1 my-1 rounded bg-primary-50\",\n            table { class: \"text-right\",\n                tbody {\n                    TableRow { name: \"Current User\", title: \"Logged-in user\",\n                        code { \"{env.current_user}\" }\n                    }\n                    TableRow { name: \"OS\", title: \"Operating System\",\n                        code { \"{env.os}\" }\n                    }\n                    TableRow { name: \"Total disk space\", title: \"Total disk space on the current machine\",\n                        code { \"{env.total_disk_space}\" }\n                    }\n                    TableRow { name: \"Total RAM\", title: \"Total memory on the current machine\",\n                        code { \"{env.total_memory}\" }\n                    }\n                }\n            }\n        }\n    }\n}\n\n#[component]\nfn TableRow(name: &'static str, title: String, children: Element) -> Element {\n    rsx! {\n        tr { title: \"{title}\",\n            td { class: \"px-4 py-2 font-semibold text-base-700\", \"{name}\" }\n            td { class: \"px-4 py-2 text-left\",\n                code { { children } }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/omnix-gui/src/app/mod.rs",
    "content": "//! Frontend UI entry point\n\n// Workaround for https://github.com/rust-lang/rust-analyzer/issues/15344\n#![allow(non_snake_case)]\n\nmod flake;\nmod health;\nmod info;\nmod state;\nmod widget;\n\nuse dioxus::prelude::*;\nuse dioxus_router::prelude::*;\nuse nix_rs::flake::url::FlakeUrl;\n\nuse crate::app::{\n    flake::{Flake, FlakeRaw},\n    health::Health,\n    info::Info,\n    state::AppState,\n    widget::{Loader, RefreshButton},\n};\n\n#[derive(Routable, PartialEq, Debug, Clone)]\n#[rustfmt::skip]\nenum Route {\n    #[layout(Wrapper)]\n        #[route(\"/\")]\n        Dashboard {},\n        #[route(\"/flake\")]\n        Flake {},\n        #[route(\"/flake/raw\")]\n        FlakeRaw {},\n        #[route(\"/health\")]\n        Health {},\n        #[route(\"/info\")]\n        Info {},\n}\n\nimpl Route {\n    fn go_to_flake(url: FlakeUrl) {\n        AppState::use_state().set_flake_url(url);\n        use_navigator().replace(Route::Flake {});\n    }\n\n    fn go_to_dashboard() {\n        AppState::use_state().reset_flake_data();\n        use_navigator().replace(Route::Dashboard {});\n    }\n}\n\n/// Main frontend application container\npub fn App() -> Element {\n    AppState::provide_state();\n    rsx! {\n        body { class: \"bg-base-100 overflow-hidden\", Router::<Route> {} }\n    }\n}\n\nfn Wrapper() -> Element {\n    rsx! {\n        div { class: \"flex flex-col text-center justify-between w-full h-screen\",\n            TopBar {}\n            div { class: \"m-2 py-2 overflow-auto\", Outlet::<Route> {} }\n            Footer {}\n        }\n    }\n}\n\n#[component]\nfn TopBar() -> Element {\n    let is_dashboard = use_route::<Route>() == Route::Dashboard {};\n    let state = AppState::use_state();\n    let health_checks = state.health_checks.read();\n    let nix_info = state.nix_info.read();\n    rsx! {\n        div { class: \"flex justify-between items-center w-full p-2 bg-primary-100 shadow\",\n            div { class: \"flex space-x-2\",\n                a {\n                    onclick: move |_| {\n                        if !is_dashboard {\n                            Route::go_to_dashboard();\n                        }\n                    },\n                    class: if is_dashboard { \"cursor-auto\" } else { \"cursor-pointer\" },\n                    \"🏠\"\n                }\n            }\n            div { class: \"flex space-x-2\",\n                ViewRefreshButton {}\n                Link { to: Route::Health {},\n                    span { title: \"Nix Health Status\",\n                        match (*health_checks).current_value() {\n                            Some(Ok(checks)) => rsx! {\n                                if checks.iter().all(|check| check.result.green()) {\n                                    \"✅\"\n                                } else {\n                                    \"❌\"\n                                }\n                            },\n                            Some(Err(err)) => rsx! { \"{err}\" },\n                            None => rsx! { Loader {} },\n                        }\n                    }\n                }\n                Link { to: Route::Info {},\n                    span {\n                        \"Nix \"\n                        match (*nix_info).current_value() {\n                            Some(Ok(info)) => rsx! {\n                                \"{info.nix_version} on {info.nix_env.os}\"\n                            },\n                            Some(Err(err)) => rsx! { \"{err}\" },\n                            None => rsx! { Loader {} },\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n/// Intended to refresh the data behind the current route.\n#[component]\nfn ViewRefreshButton() -> Element {\n    let state = AppState::use_state();\n    let (busy, mut refresh_signal) = match use_route() {\n        Route::Flake {} => Some((\n            state.flake.read().is_loading_or_refreshing(),\n            state.flake_refresh,\n        )),\n        Route::Health {} => Some((\n            state.health_checks.read().is_loading_or_refreshing(),\n            state.health_checks_refresh,\n        )),\n        Route::Info {} => Some((\n            state.nix_info.read().is_loading_or_refreshing(),\n            state.nix_info_refresh,\n        )),\n        _ => None,\n    }?;\n    rsx! {\n        { RefreshButton (\n            busy,\n            move |_| {\n                refresh_signal.write().request_refresh();\n            }\n        ) }\n    }\n}\n\n#[component]\nfn Footer() -> Element {\n    rsx! {\n        footer { class: \"flex flex-row justify-center w-full bg-primary-100 p-2\",\n            a { href: \"https://github.com/juspay/omnix\",\n                img { src: \"images/128x128.png\", class: \"h-4\" }\n            }\n        }\n    }\n}\n\n// Home page\nfn Dashboard() -> Element {\n    tracing::debug!(\"Rendering Dashboard page\");\n    let state = AppState::use_state();\n    rsx! {\n        div { class: \"pl-4\",\n            h2 { class: \"text-2xl\", \"Enter a flake URL:\" }\n            { flake::FlakeInput () },\n            h2 { class: \"text-2xl\", \"Or, try one of these:\" }\n            div { class: \"flex flex-col\",\n                for flake_url in state.flake_cache.read().recent_flakes() {\n                    a {\n                        onclick: move |_| {\n                            Route::go_to_flake(flake_url.clone());\n                        },\n                        class: \"cursor-pointer text-primary-600 underline hover:no-underline\",\n                        \"{flake_url.clone()}\"\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crates/omnix-gui/src/app/state/datum.rs",
    "content": "use std::{fmt::Display, future::Future};\n\nuse dioxus::prelude::*;\nuse dioxus_signals::{CopyValue, Signal};\nuse tokio::task::AbortHandle;\n\n/// Represent loading/refreshing state of UI data\n#[derive(Debug, Clone, Copy, PartialEq)]\npub struct Datum<T> {\n    /// The current value of the datum\n    value: Option<T>,\n    /// If the datum is currently being loaded or refresh, this contains the\n    /// [AbortHandle] to abort that loading/refreshing process.\n    task: CopyValue<Option<AbortHandle>>,\n}\n\nimpl<T> Default for Datum<T> {\n    fn default() -> Self {\n        Self {\n            value: None,\n            task: CopyValue::default(),\n        }\n    }\n}\n\nimpl<T> Datum<T> {\n    pub fn is_loading_or_refreshing(&self) -> bool {\n        self.task.read().is_some()\n    }\n\n    /// Get the inner value if available\n    pub fn current_value(&self) -> Option<&T> {\n        self.value.as_ref()\n    }\n\n    pub async fn set_value(signal: &mut Signal<Datum<T>>, value: T)\n    where\n        T: Send + Clone + 'static,\n    {\n        Datum::refresh_with(signal, async { value }).await;\n    }\n\n    /// Refresh the datum [Signal] using the given function\n    ///\n    /// If a previous refresh is still running, it will be cancelled.\n    pub async fn refresh_with<F>(signal: &mut Signal<Datum<T>>, f: F) -> Option<T>\n    where\n        F: Future<Output = T> + Send + 'static,\n        T: Send + Clone + 'static,\n    {\n        // Cancel existing fetcher if any.\n        signal.with_mut(move |x| {\n            if let Some(abort_handle) = x.task.take() {\n                tracing::warn!(\n                    \"🍒 Cancelling previous refresh task for {}\",\n                    std::any::type_name::<T>()\n                );\n                abort_handle.abort();\n            }\n        });\n\n        // NOTE: We must spawn a tasks (using tokio::spawn), otherwise this\n        // will run in main desktop thread, and will hang at some point.\n        let join_handle = tokio::spawn(f);\n\n        // Store the [AbortHandle] for cancelling latter.\n        let abort_handle = join_handle.abort_handle();\n        signal.with_mut(move |x| {\n            *x.task.write() = Some(abort_handle);\n        });\n\n        // Wait for result and update the signal state.\n        match join_handle.await {\n            Ok(val) => {\n                signal.with_mut(|x| {\n                    tracing::debug!(\"🍒 Setting {} datum value\", std::any::type_name::<T>());\n                    x.value = Some(val.clone());\n                    *x.task.write() = None;\n                });\n                Some(val)\n            }\n            Err(err) => {\n                if !err.is_cancelled() {\n                    tracing::error!(\"🍒 Datum refresh failed: {err}\");\n                    signal.with_mut(move |x| {\n                        *x.task.write() = None;\n                    });\n                }\n                // x.task will be set to None by the caller who cancelled us, so\n                // we need not do anything here.\n                None\n            }\n        }\n    }\n}\n\nimpl<T, E: Display> Datum<Result<T, E>> {\n    /// Render the result datum with the given component\n    ///\n    /// The error message will be rendered appropriately. If the datum is\n    /// unavailable, nothing will be rendered (loading state is rendered\n    /// differently)\n    pub fn render_with<F>(&self, component: F) -> Element\n    where\n        F: FnOnce(&T) -> Element,\n    {\n        match self.current_value()? {\n            Ok(value) => component(value),\n            Err(err) => rsx! {\n                div { class: \"p-4 my-1 text-left text-sm font-mono text-white bg-red-500 rounded\",\n                    \"Error: {err}\"\n                }\n            },\n        }\n    }\n}\n"
  },
  {
    "path": "crates/omnix-gui/src/app/state/db.rs",
    "content": "//! A database of [Flake] intended to be cached in dioxus [Signal] and persisted to disk.\n//!\n//! This is purposefully dumb right now, but we might revisit this in future based on actual performance.\n\nuse serde::{Deserialize, Serialize};\nuse std::{collections::HashMap, time::SystemTime};\n\nuse dioxus_sdk::storage::new_storage;\nuse dioxus_sdk::storage::LocalStorage;\nuse dioxus_signals::Signal;\n\nuse crate::app::state::FlakeUrl;\nuse nix_rs::flake::Flake;\n\n/// A database of [Flake] intended to be cached in dioxus [Signal] and persisted to disk.\n///\n/// Contains the \"last fetched\" time and the [Flake] itself.\n#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)]\npub struct FlakeCache(HashMap<FlakeUrl, Option<(SystemTime, Flake)>>);\n\nimpl FlakeCache {\n    /// Create a new [Signal] for [FlakeCache] from [LocalStorage].\n    pub fn new_signal() -> Signal<FlakeCache> {\n        new_storage::<LocalStorage, _>(\"flake_cache\".to_string(), || {\n            tracing::warn!(\"📦 No flake cache found\");\n            let init = FlakeUrl::suggestions()\n                .into_iter()\n                .map(|url| (url, None))\n                .collect();\n            FlakeCache(init)\n        })\n    }\n\n    /// Look up a [Flake] by [FlakeUrl] in the cache.\n    pub fn get(&self, k: &FlakeUrl) -> Option<Flake> {\n        let (t, flake) = self.0.get(k).and_then(|v| v.as_ref().cloned())?;\n        tracing::info!(\"Cache hit for {} (updated: {:?})\", k, t);\n        Some(flake)\n    }\n\n    /// Update the cache with a new [Flake].\n    pub fn update(&mut self, k: FlakeUrl, flake: Flake) {\n        tracing::info!(\"Caching flake [{}]\", &k);\n        self.0.insert(k, Some((SystemTime::now(), flake)));\n    }\n\n    /// Recently updated flakes, along with any unavailable flakes in cache.\n    pub fn recent_flakes(&self) -> Vec<FlakeUrl> {\n        let mut pairs: Vec<_> = self\n            .0\n            .iter()\n            .map(|(k, v)| (k, v.as_ref().map(|(t, _)| t)))\n            .collect();\n\n        // Sort by the timestamp in descending order.\n        pairs.sort_unstable_by(|a, b| b.1.cmp(&a.1));\n\n        pairs.into_iter().map(|(k, _)| k.clone()).collect()\n    }\n}\n"
  },
  {
    "path": "crates/omnix-gui/src/app/state/error.rs",
    "content": "use std::fmt::Display;\n\n/// Catch all error to use in UI components\n#[derive(Debug, Clone, PartialEq, Eq, Hash)]\npub struct SystemError {\n    pub message: String,\n}\n\nimpl Display for SystemError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.write_str(&self.message)\n    }\n}\n\nimpl From<String> for SystemError {\n    fn from(message: String) -> Self {\n        Self { message }\n    }\n}\n"
  },
  {
    "path": "crates/omnix-gui/src/app/state/mod.rs",
    "content": "//! Application state\n\nmod datum;\nmod db;\nmod error;\nmod refresh;\n\nuse dioxus::prelude::*;\nuse dioxus_signals::{Readable, Signal, Writable};\nuse nix_rs::{\n    config::NixConfig,\n    flake::{url::FlakeUrl, Flake},\n    info::NixInfo,\n    version::NixVersion,\n};\nuse omnix_health::NixHealth;\n\nuse self::{datum::Datum, error::SystemError, refresh::Refresh};\n\n/// Our dioxus application state is a struct of [Signal]s that store app state.\n///\n/// They use [Datum] which is a glorified [Option] to distinguish between initial\n/// loading and subsequent refreshing.\n#[derive(Default, Clone, Copy, Debug, PartialEq)]\npub struct AppState {\n    /// [NixInfo] as detected on the user's system\n    pub nix_info: Signal<Datum<Result<NixInfo, SystemError>>>,\n    pub nix_info_refresh: Signal<Refresh>,\n\n    /// User's Nix health [omnix_health::traits::Check]s\n    pub health_checks: Signal<Datum<Result<Vec<omnix_health::traits::Check>, SystemError>>>,\n    pub health_checks_refresh: Signal<Refresh>,\n\n    /// User selected [FlakeUrl]\n    pub flake_url: Signal<Option<FlakeUrl>>,\n    /// Trigger to refresh [AppState::flake]\n    pub flake_refresh: Signal<Refresh>,\n    /// [Flake] for [AppState::flake_url]\n    pub flake: Signal<Datum<Result<Flake, SystemError>>>,\n\n    /// Cached [Flake] values indexed by [FlakeUrl]\n    ///\n    /// Most recently updated flakes appear first.\n    pub flake_cache: Signal<db::FlakeCache>,\n}\n\nimpl AppState {\n    fn new() -> Self {\n        tracing::info!(\"🔨 Creating new AppState\");\n        // TODO: Should we use new_synced_storage, instead? To allow multiple app windows?\n        let flake_cache = db::FlakeCache::new_signal();\n        AppState {\n            flake_cache,\n            ..AppState::default()\n        }\n    }\n\n    /// Get the [AppState] from context\n    pub fn use_state() -> Self {\n        use_context::<Self>()\n    }\n\n    pub fn provide_state() {\n        tracing::debug!(\"🏗️ Providing AppState\");\n        let mut state = use_context_provider(Self::new);\n        // FIXME: Can we avoid calling build_network multiple times?\n        state.build_network();\n    }\n\n    /// Return the [String] representation of the current [AppState::flake_url] value. If there is none, return empty string.\n    pub fn get_flake_url_string(&self) -> String {\n        self.flake_url\n            .read()\n            .clone()\n            .map_or(\"\".to_string(), |url| url.to_string())\n    }\n\n    pub fn set_flake_url(&mut self, url: FlakeUrl) {\n        tracing::info!(\"setting flake url to {}\", &url);\n        self.flake_url.set(Some(url));\n    }\n\n    /// Empty flake related data (`flake_url` and `flake`)\n    pub fn reset_flake_data(&mut self) {\n        tracing::info!(\"empty flake data\");\n        self.flake.set(Datum::default());\n        self.flake_url.set(None);\n    }\n}\n\nimpl AppState {\n    /// Build the Signal network\n    ///\n    /// If a signal's value is dependent on another signal's value, you must\n    /// define that relationship here.\n    fn build_network(&mut self) {\n        tracing::debug!(\"🕸️ Building AppState network\");\n        // Build `state.flake` signal dependent signals change\n        {\n            // ... when [AppState::flake_url] changes.\n            let flake_url = self.flake_url;\n            let flake_cache = self.flake_cache;\n            let mut flake_refresh = self.flake_refresh;\n            let mut flake = self.flake;\n            let _ = use_resource(move || async move {\n                if let Some(flake_url) = flake_url.read().clone() {\n                    let maybe_flake = flake_cache.read().get(&flake_url);\n                    if let Some(cached_flake) = maybe_flake {\n                        Datum::set_value(&mut flake, Ok(cached_flake)).await;\n                    } else {\n                        flake_refresh.write().request_refresh();\n                    }\n                }\n            });\n            // ... when refresh button is clicked.\n            let flake_refresh = self.flake_refresh;\n            let flake_url = self.flake_url;\n            let mut flake = self.flake;\n            let mut flake_cache = self.flake_cache;\n            let _ = use_resource(move || async move {\n                let nixcmd = nix_rs::command::NixCmd::get().await;\n                let flake_url = flake_url.read().clone();\n                let refresh = *flake_refresh.read();\n                if let Some(flake_url) = flake_url {\n                    let flake_url_2 = flake_url.clone();\n                    tracing::info!(\"Updating flake [{}] refresh={} ...\", &flake_url, refresh);\n                    let res = Datum::refresh_with(&mut flake, async move {\n                        let nix_version = NixVersion::from_nix(nixcmd)\n                            .await\n                            .map_err(|e| Into::<SystemError>::into(e.to_string()))?;\n                        let nix_config = NixConfig::from_nix(nixcmd, &nix_version)\n                            .await\n                            .map_err(|e| Into::<SystemError>::into(e.to_string()))?;\n                        Flake::from_nix(nixcmd, &nix_config, flake_url_2)\n                            .await\n                            .map_err(|e| Into::<SystemError>::into(e.to_string()))\n                    })\n                    .await;\n                    if let Some(Ok(flake)) = res {\n                        flake_cache.with_mut(|cache| {\n                            cache.update(flake_url, flake);\n                        });\n                    }\n                }\n            });\n        }\n\n        // Build `state.health_checks`\n        {\n            let nix_info = self.nix_info;\n            let health_checks_refresh = self.health_checks_refresh;\n            let mut health_checks = self.health_checks;\n            let _ = use_resource(move || async move {\n                let nix_info = nix_info.read().clone();\n                let refresh = *health_checks_refresh.read();\n                if let Some(nix_info) = nix_info.current_value().map(|x| {\n                    x.as_ref()\n                        .map_err(|e| Into::<SystemError>::into(e.to_string()))\n                        .cloned()\n                }) {\n                    tracing::info!(\"Updating nix health [{}] ...\", refresh);\n                    Datum::refresh_with(&mut health_checks, async move {\n                        let health_checks = NixHealth::default().run_checks(&nix_info?, None);\n                        Ok(health_checks)\n                    })\n                    .await;\n                }\n            });\n        }\n\n        // Build `state.nix_info`\n        {\n            let mut nix_info = self.nix_info;\n            let nix_info_refresh = self.nix_info_refresh;\n            let _ = use_resource(move || async move {\n                let refresh = *nix_info_refresh.read();\n                tracing::info!(\"Updating nix info [{}] ...\", refresh);\n                Datum::refresh_with(&mut nix_info, async {\n                    let ver = NixVersion::get()\n                        .await\n                        .as_ref()\n                        .map_err(|e| Into::<SystemError>::into(e.to_string()))?;\n                    let cfg = NixConfig::get()\n                        .await\n                        .as_ref()\n                        .map_err(|e| Into::<SystemError>::into(e.to_string()))?;\n                    NixInfo::new(*ver, cfg.clone())\n                        .await\n                        .map_err(|e| SystemError {\n                            message: format!(\"Error getting nix info: {:?}\", e),\n                        })\n                })\n                .await;\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "crates/omnix-gui/src/app/state/refresh.rs",
    "content": "use std::fmt::Display;\n\n/// Represents an user request to update some thing (a dioxus Signal)\n#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]\npub struct Refresh {\n    idx: usize,\n}\n\nimpl Display for Refresh {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", self.idx)\n    }\n}\n\nimpl Refresh {\n    pub fn request_refresh(&mut self) {\n        tracing::info!(\"🔄 Requesting refresh of a signal\");\n        self.idx += 1;\n    }\n}\n"
  },
  {
    "path": "crates/omnix-gui/src/app/widget.rs",
    "content": "//! Various widgets\n\nuse std::path::PathBuf;\n\nuse dioxus::prelude::*;\n\n/// A refresh button with a busy indicator\npub fn RefreshButton<F: 'static + FnMut(Event<MouseData>)>(busy: bool, mut handler: F) -> Element {\n    rsx! {\n        button {\n            disabled: busy,\n            onclick: move |evt| {\n                if !busy {\n                    handler(evt)\n                }\n            },\n            title: \"Refresh current data being viewed\",\n            LoaderIcon { loading: busy }\n        }\n    }\n}\n\n/// A button that opens a file explorer dialog.\n///\n/// Note: You can only select a single folder.\n///\n/// NOTE(for future): When migrating to Dioxus using Tauri 2.0, switch to using\n/// <https://github.com/tauri-apps/tauri-plugin-dialog>\n// #[component]\npub fn FolderDialogButton<F: 'static + FnMut(PathBuf)>(mut handler: F) -> Element {\n    // FIXME: The id should be unique if this widget is used multiple times on\n    // the same page.\n    let id = \"folder-dialog-input\";\n    rsx! {\n        input {\n            r#type: \"file\",\n            multiple: false,\n            directory: true,\n            accept: \"\",\n            oninput: move |evt: Event<FormData>| {\n                if let Some(path) = get_selected_path(evt) {\n                    handler(path)\n                }\n            },\n            id,\n            class: \"hidden\"\n        }\n        label {\n            class: \"py-1 px-1 cursor-pointer hover:scale-125 active:scale-100\",\n            r#for: id,\n            title: \"Choose a local folder\",\n            \"📁\"\n        }\n    }\n}\n\n/// Get the user selected path from a file dialog event\n///\n/// If the user has not selected any (eg: cancels the dialog), this returns\n/// None. Otherwise, it returns the first entry in the selected list.\nfn get_selected_path(evt: Event<FormData>) -> Option<PathBuf> {\n    match evt.files().as_ref() {\n        None => {\n            tracing::error!(\"unable to get files from event\");\n            None\n        }\n        Some(file_engine) => {\n            let path = file_engine.files().first().cloned()?;\n            Some(PathBuf::from(path))\n        }\n    }\n}\n\n#[component]\npub fn Loader() -> Element {\n    rsx! {\n        div { class: \"flex justify-center items-center\",\n            div { class: \"animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-purple-500\" }\n        }\n    }\n}\n\n#[component]\npub fn LoaderIcon(loading: bool) -> Element {\n    let cls = if loading {\n        \"animate-spin text-base-800\"\n    } else {\n        \"text-primary-700 hover:text-primary-500\"\n    };\n    rsx! {\n        div { class: cls,\n            svg {\n                class: \"h-6 w-6 scale-x-[-1]\",\n                xmlns: \"http://www.w3.org/2000/svg\",\n                view_box: \"0 0 24 24\",\n                fill: \"none\",\n                stroke: \"currentColor\",\n                path {\n                    d: \"M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15\",\n                    stroke_linecap: \"round\",\n                    stroke_linejoin: \"round\",\n                    stroke_width: \"2\"\n                }\n            }\n        }\n    }\n}\n\n/// A div that can get scrollbar for long content\n///\n/// Since our body container is `overflow-hidden`, we need to wrap content that\n/// can get long in this component.\n#[component]\n#[allow(dead_code)] // https://github.com/juspay/omnix/issues/132\npub fn Scrollable(children: Element) -> Element {\n    rsx! {\n        div { class: \"overflow-auto\", { children } }\n    }\n}\n"
  },
  {
    "path": "crates/omnix-gui/src/cli.rs",
    "content": "//! Command-line interface\nuse clap::Parser;\nuse clap_verbosity_flag::{InfoLevel, Verbosity};\n\n#[derive(Parser, Debug)]\npub struct Args {\n    #[command(flatten)]\n    pub verbosity: Verbosity<InfoLevel>,\n}\n"
  },
  {
    "path": "crates/omnix-gui/src/main.rs",
    "content": "#![feature(let_chains)]\nuse dioxus::prelude::*;\nuse dioxus_desktop::{LogicalSize, WindowBuilder};\n\nmod app;\nmod cli;\n\nfn main() {\n    use clap::Parser;\n    let args = crate::cli::Args::parse();\n    omnix_common::logging::setup_logging(&args.verbosity, false);\n\n    // Set data directory for persisting [Signal]s. On macOS, this is ~/Library/Application Support/omnix-gui.\n    dioxus_sdk::storage::set_dir!();\n\n    let config = dioxus_desktop::Config::new()\n        .with_custom_head(r#\" <link rel=\"stylesheet\" href=\"tailwind.css\"> \"#.to_string())\n        .with_window(\n            WindowBuilder::new()\n                .with_title(\"Omnix\")\n                .with_inner_size(LogicalSize::new(800, 700)),\n        );\n    LaunchBuilder::desktop().with_cfg(config).launch(app::App)\n}\n"
  },
  {
    "path": "crates/omnix-gui/tailwind.config.js",
    "content": "const colors = require('tailwindcss/colors')\nconst defaultTheme = require('tailwindcss/defaultTheme')\n\nmodule.exports = {\n  content: [\n    \"./src/**/*.rs\",\n    \"./dist/**/*.html\"\n  ],\n  theme: {\n    fontFamily: {\n      sans: ['Proxima Nova', ...defaultTheme.fontFamily.sans],\n      mono: [...defaultTheme.fontFamily.mono],\n      serif: [...defaultTheme.fontFamily.serif]\n    },\n    extend: {\n      // Our application colour palette is defined here.\n      colors: {\n        'base': colors.gray,\n        'primary': colors.blue,\n        'secondary': colors.yellow,\n        'error': colors.red\n      }\n    }\n  },\n  plugins: [],\n}\n"
  },
  {
    "path": "crates/omnix-health/CHANGELOG.md",
    "content": "# Changelog\n\n## Unreleased\n\n- Crate renamed to `omnix-health`\n- Remove unused `logging` module\n- Display Nix installer used\n- `nix-version.min-required` -> `nix-version.supported`\n\n## 1.0.0\n\n- Remove executable (use `omnix` instead)\n* **new**:\n  * Expose `run_checks_with`\n* **minor**:\n  * Remove redundant `NixEnv` detection\n- **fixes**:\n  - Error out when the user passes `.#foo` as flake URL argument, with `foo` missing in the flake.nix. Previously, this fell back to `.#default` configuration.\n\n## [0.4.0](https://github.com/juspay/nix-health/compare/0.3.0...0.4.0) (2024-07-10)\n\n### Features\n\n* **lib:** Expose `print_returning_exit_code`  (#25)\n([b9c70a9](https://github.com/juspay/nix-health/commit/b9c70a9506823bdcc1d54c14b7c56d299b3a5c6a)),\ncloses [#25](https://github.com/juspay/nix-health/issues/25)\n* build linux static executable\n([78b95e8](https://github.com/juspay/nix-health/commit/78b95e8528282ef3f88b2ed29c0f5fc0cebbaa07))\n* Add flake-module to run nix-health in devShell shellHook\n([2f8d8dc](https://github.com/juspay/nix-health/commit/2f8d8dc30121923192c78a8f5152c5c89fdf1809))\n\n### Fixes\n\n* build failure on intel mac\n([91e9bcf](https://github.com/juspay/nix-health/commit/91e9bcfd60d672074951d534d7b51f609dda1e94))\n\n## 0.3.0 (2024-07-10)\n\n### Fixes\n\n* **nix-health:** use `direnv status --json` & create `direnv` crate (#123)\n([f7762d7](https://github.com/juspay/nix-health/commit/f7762d7fec28f3091289fb03b3ad171cfb923f87)),\ncloses [#123](https://github.com/juspay/nix-health/issues/123)\n"
  },
  {
    "path": "crates/omnix-health/Cargo.toml",
    "content": "[package]\nname = \"omnix-health\"\nversion = \"1.3.2\"\nlicense = \"Apache-2.0\"\nrepository = \"https://github.com/juspay/omnix\"\ndescription = \"Check the health of your Nix setup\"\nedition = \"2021\"\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nserde = { workspace = true }\ncfg-if = { workspace = true }\nclap = { workspace = true }\nomnix-common = { workspace = true }\nregex = { workspace = true }\nthiserror = { workspace = true }\nserde_json = { workspace = true }\nserde_with = { workspace = true }\ntokio = { workspace = true }\ntracing = { workspace = true }\nurl = { workspace = true }\nnix_rs = { workspace = true }\ndirenv = { workspace = true }\nhuman-panic = { workspace = true }\nanyhow = { workspace = true }\ncolored = { workspace = true }\nwhich = { workspace = true }\nbytesize = { workspace = true }\nsemver = { workspace = true }\nserde_repr = { workspace = true }\n"
  },
  {
    "path": "crates/omnix-health/README.md",
    "content": "# `omnix-health`\n\n[![Crates.io](https://img.shields.io/crates/v/omnix-health.svg)](https://crates.io/crates/omnix-health)\n\nThe Rust crate responsible for [`om health`](https://omnix.page/om/health.html).\n"
  },
  {
    "path": "crates/omnix-health/crate.nix",
    "content": "{\n  autoWire = [ ];\n  crane = {\n    args = {\n      nativeBuildInputs = [\n        # nix # Tests need nix cli\n      ];\n    };\n  };\n}\n"
  },
  {
    "path": "crates/omnix-health/failing/.envrc",
    "content": "# Just a dummy .envrc for testing flake.nix checks\ninvalid"
  },
  {
    "path": "crates/omnix-health/failing/flake.nix",
    "content": "# Just a flake.nix to configure omnix-health to fail all possible checks\n#\n# Used for testing purposes; run as:\n#   cargo run -p omnix-cli health ./crates/omnix-health/failing\n\n{\n  outputs = _: {\n    om.health.default = {\n      caches.required = [ \"https://unknown.cachix.org\" \"https://example.com\" ];\n      nix-version.min-required = \"9.99.99\";\n      system = {\n        min_ram = \"512GB\";\n        min_disk_space = \"64TB\";\n      };\n    };\n  };\n}\n"
  },
  {
    "path": "crates/omnix-health/module/flake-module.nix",
    "content": "{ self, lib, flake-parts-lib, ... }:\n\nlet\n  inherit (flake-parts-lib)\n    mkPerSystemOption;\nin\n{\n  options = {\n    perSystem = mkPerSystemOption\n      ({ config, pkgs, ... }: {\n        options.nix-health.outputs.devShell = lib.mkOption {\n          type = lib.types.package;\n          description = ''\n            Add a shellHook for running nix-health on the flake.\n          '';\n          default = pkgs.mkShell {\n            shellHook = ''\n              # Must use a subshell so that 'trap' handles only nix-health\n              # crashes.\n              (\n                trap \"${lib.getExe pkgs.toilet} NIX UNHEALTHY --filter gay -f smmono9\" ERR\n\n                ${lib.getExe pkgs.nix-health} --quiet .\n              )\n            '';\n          };\n        };\n      });\n  };\n}\n"
  },
  {
    "path": "crates/omnix-health/module/flake.nix",
    "content": "{\n  outputs = _: {\n    flakeModule = ./flake-module.nix;\n  };\n}\n"
  },
  {
    "path": "crates/omnix-health/src/check/caches.rs",
    "content": "use nix_rs::info;\nuse serde::{Deserialize, Serialize};\nuse url::Url;\n\nuse crate::traits::*;\n\n/// Check that [nix_rs::config::NixConfig::substituters] is set to a good value.\n#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]\n#[serde(rename_all = \"kebab-case\")]\n#[serde(default)]\npub struct Caches {\n    pub required: Vec<Url>,\n}\n\nimpl Default for Caches {\n    fn default() -> Self {\n        Caches {\n            required: vec![Url::parse(\"https://cache.nixos.org\").unwrap()],\n        }\n    }\n}\n\nimpl Checkable for Caches {\n    fn check(\n        &self,\n        nix_info: &info::NixInfo,\n        _: Option<&nix_rs::flake::url::FlakeUrl>,\n    ) -> Vec<(&'static str, Check)> {\n        let missing_caches = self.get_missing_caches(nix_info);\n        let result = if missing_caches.is_empty() {\n            CheckResult::Green\n        } else {\n            CheckResult::Red {\n                msg: format!(\n                    \"You are missing some required caches: {}\",\n                    missing_caches\n                        .iter()\n                        .map(|url| url.to_string())\n                        .collect::<Vec<_>>()\n                        .join(\" \")\n                ),\n                suggestion: format!(\n                    \"Caches can be added in your {} (see https://nixos.wiki/wiki/Binary_Cache#Using_a_binary_cache). Cachix caches can also be added using `nix run nixpkgs#cachix use <name>`.\",\n                    nix_info.nix_env.os.nix_config_label()\n                )\n            }\n        };\n        let check = Check {\n            title: \"Nix Caches in use\".to_string(),\n            info: format!(\n                \"substituters = {}\",\n                nix_info\n                    .nix_config\n                    .substituters\n                    .value\n                    .iter()\n                    .map(|url| url.to_string())\n                    .collect::<Vec<_>>()\n                    .join(\" \")\n            ),\n            result,\n            required: true,\n        };\n\n        vec![(\"caches\", check)]\n    }\n}\n\nimpl Caches {\n    /// Get subset of required caches not already in use\n    pub fn get_missing_caches(&self, nix_info: &info::NixInfo) -> Vec<Url> {\n        let val = &nix_info.nix_config.substituters.value;\n        self.required\n            .iter()\n            .filter(|required_cache| !val.contains(required_cache))\n            .cloned()\n            .collect()\n    }\n}\n\npub struct CachixCache(pub String);\n\nimpl CachixCache {\n    /// Parse the https URL into a CachixCache\n    pub fn from_url(url: &Url) -> Option<Self> {\n        // Parse https://foo.cachix.org into CachixCache(\"foo\")\n        // If domain is not cachix.org, return None.\n        let host = url.host_str()?;\n        if host.ends_with(\".cachix.org\") {\n            Some(CachixCache(host.split('.').next()?.to_string()))\n        } else {\n            None\n        }\n    }\n\n    /// Run `cachix use` for this cache\n    pub async fn cachix_use(&self) -> anyhow::Result<()> {\n        let mut cmd = tokio::process::Command::new(env!(\"CACHIX_BIN\"));\n        cmd.arg(\"use\").arg(&self.0);\n        let status = cmd.spawn()?.wait().await?;\n        if !status.success() {\n            anyhow::bail!(\"Failed to run `cachix use {}`\", self.0);\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/omnix-health/src/check/direnv.rs",
    "content": "use nix_rs::{flake::url::FlakeUrl, info};\nuse serde::{Deserialize, Serialize};\n\nuse crate::traits::{Check, CheckResult, Checkable};\n\n/// Check if direnv is installed\n#[derive(Debug, Serialize, Deserialize, Clone)]\n#[serde(default, rename_all = \"kebab-case\")]\npub struct Direnv {\n    pub(crate) enable: bool,\n    /// Whether to produce [Check::required] checks\n    pub(crate) required: bool,\n}\n\nimpl Default for Direnv {\n    fn default() -> Self {\n        Self {\n            enable: true,\n            required: false,\n        }\n    }\n}\n\nimpl Checkable for Direnv {\n    fn check(\n        &self,\n        _nix_info: &info::NixInfo,\n        flake_url: Option<&FlakeUrl>,\n    ) -> Vec<(&'static str, Check)> {\n        let mut checks = vec![];\n        if !self.enable {\n            return checks;\n        }\n\n        let direnv_install_result = direnv::DirenvInstall::detect();\n        checks.push((\n            \"direnv-install-check\",\n            install_check(&direnv_install_result, self.required),\n        ));\n\n        match direnv_install_result.as_ref() {\n            Err(_) => return checks,\n            Ok(direnv_install) => {\n                // If direnv is installed, check for version and then allowed_check\n                // This check is currently only relevant if the flake is local and an `.envrc` exists.\n                match flake_url.as_ref().and_then(|url| url.as_local_path()) {\n                    None => {}\n                    Some(local_path) => {\n                        if local_path.join(\".envrc\").exists() {\n                            checks.push((\n                                \"direnv-allowed-check\",\n                                allowed_check(direnv_install, local_path, self.required),\n                            ));\n                        }\n                    }\n                }\n            }\n        }\n\n        checks\n    }\n}\n\n/// [Check] that direnv was installed.\nfn install_check(\n    direnv_install_result: &Result<direnv::DirenvInstall, direnv::DirenvInstallError>,\n    required: bool,\n) -> Check {\n    let setup_url = \"https://github.com/juspay/nixos-unified-template\";\n    Check {\n        title: \"Direnv installation\".to_string(),\n        info: format!(\n            \"direnv location = {:?}\",\n            direnv_install_result.as_ref().ok().map(|s| &s.bin_path)\n        ),\n        result: match direnv_install_result {\n            Ok(direnv_install) if is_path_in_nix_store(&direnv_install.canonical_path) => {\n                CheckResult::Green\n            }\n            Ok(direnv_install) => CheckResult::Red {\n                msg: format!(\n                    \"direnv is installed outside of Nix ({:?})\",\n                    &direnv_install.canonical_path\n                ),\n                suggestion: format!(\n                    \"Install direnv via Nix, it will also manage shell integration. See <{}>\",\n                    setup_url\n                ),\n            },\n            Err(e) => CheckResult::Red {\n                msg: format!(\"Unable to locate direnv ({})\", e),\n                suggestion: format!(\"Install direnv & nix-direnv. See <{}>\", setup_url),\n            },\n        },\n        required,\n    }\n}\n\n/// Check that the path is in the Nix store (usually /nix/store)\npub fn is_path_in_nix_store(path: &std::path::Path) -> bool {\n    path.starts_with(\"/nix/store\")\n}\n\n/// [Check] that direnv was allowed on the local flake\nfn allowed_check(\n    direnv_install: &direnv::DirenvInstall,\n    local_flake: &std::path::Path,\n    required: bool,\n) -> Check {\n    let suggestion = format!(\n        \"Run `direnv allow` under `{}` (this activates the Nix devshell automatically)\",\n        local_flake.display()\n    );\n    let direnv_allowed = direnv_install\n        .status(local_flake)\n        .map(|status| status.state.is_allowed());\n    Check {\n        title: \"Direnv allowed\".to_string(),\n        info: format!(\"Local flake: {:?} (has .envrc)\", local_flake),\n        result: match direnv_allowed {\n            Ok(true) => CheckResult::Green,\n            Ok(false) => CheckResult::Red {\n                msg: \"direnv was not allowed on this project\".to_string(),\n                suggestion,\n            },\n            Err(e) => CheckResult::Red {\n                msg: format!(\"Unable to check direnv status: {}\", e),\n                suggestion,\n            },\n        },\n        required,\n    }\n}\n"
  },
  {
    "path": "crates/omnix-health/src/check/flake_enabled.rs",
    "content": "use nix_rs::info;\nuse serde::{Deserialize, Serialize};\n\nuse crate::traits::*;\n\n/// Check that [nix_rs::config::NixConfig::experimental_features] is set to a good value.\n#[derive(Debug, Default, Serialize, Deserialize, Clone)]\n#[serde(default)]\npub struct FlakeEnabled {}\n\nimpl Checkable for FlakeEnabled {\n    fn check(\n        &self,\n        nix_info: &info::NixInfo,\n        _: Option<&nix_rs::flake::url::FlakeUrl>,\n    ) -> Vec<(&'static str, Check)> {\n        let val = &nix_info.nix_config.experimental_features.value;\n        let check = Check {\n            title: \"Flakes Enabled\".to_string(),\n            info: format!(\"experimental-features = {}\", val.join(\" \")),\n            result: if val.contains(&\"flakes\".to_string())\n                && val.contains(&\"nix-command\".to_string())\n            {\n                CheckResult::Green\n            } else {\n                CheckResult::Red {\n                    msg: \"Nix flakes are not enabled\".into(),\n                    suggestion: \"See https://nixos.wiki/wiki/Flakes#Enable_flakes\".into(),\n                }\n            },\n            required: true,\n        };\n\n        vec![(\"flake-enabled\", check)]\n    }\n}\n"
  },
  {
    "path": "crates/omnix-health/src/check/homebrew.rs",
    "content": "use nix_rs::{flake::url::FlakeUrl, info::NixInfo};\nuse omnix_common::check::which_strict;\nuse serde::{Deserialize, Serialize};\n\nuse crate::traits::{Check, CheckResult, Checkable};\n\n/// Check if Homebrew is installed\n#[derive(Default, Debug, Serialize, Deserialize, Clone)]\n#[serde(default, rename_all = \"kebab-case\")]\npub struct Homebrew {\n    pub(crate) enable: bool,\n    pub(crate) required: bool,\n}\n\nimpl Checkable for Homebrew {\n    fn check(\n        &self,\n        nix_info: &NixInfo,\n        _flake_url: Option<&FlakeUrl>,\n    ) -> Vec<(&'static str, Check)> {\n        let mut checks = vec![];\n\n        if self.enable && matches!(nix_info.nix_env.os, nix_rs::env::OS::MacOS { .. }) {\n            checks.push((\n                \"homebrew-check\",\n                installation_check(&HomebrewInstall::detect(), self.required),\n            ));\n        }\n\n        checks\n    }\n}\n\n/// Information about user's homebrew installation.\n#[derive(Debug)]\npub struct HomebrewInstall {\n    /// The path to the Homebrew binary.\n    pub bin_path: std::path::PathBuf,\n}\n\nimpl HomebrewInstall {\n    /// Detect homebrew installation.\n    pub fn detect() -> Option<Self> {\n        which_strict(\"brew\").map(|bin_path| HomebrewInstall { bin_path })\n    }\n}\n\n/// Create a [Check] for Homebrew installation\nfn installation_check(homebrew_result: &Option<HomebrewInstall>, required: bool) -> Check {\n    let nix_setup_url = \"https://github.com/juspay/nixos-unified-template\";\n\n    Check {\n        title: \"Homebrew installation\".to_string(),\n        info: format!(\n            \"Homebrew binary: {}\",\n            homebrew_result\n                .as_ref()\n                .map(|h| format!(\"Found at {:?}\", h.bin_path))\n                .unwrap_or_else(|| \"Not found\".to_string())\n        ),\n        result: match homebrew_result {\n            Some(homebrew) => CheckResult::Red {\n                msg: format!(\n                    \"Homebrew is installed at {:?}. Consider using Nix for better reproducibility\",\n                    homebrew.bin_path\n                ),\n                suggestion: format!(\n                    \"Managing packages with Nix, rather than Homebrew, provides better reproducibility and integration. See <{}>\\n\\n{}\",\n                    nix_setup_url,\n                    HOMEBREW_REMOVAL_INSTRUCTIONS\n                ),\n            },\n            None => CheckResult::Green,\n        },\n        required,\n    }\n}\n\n/// A string containing step-by-step removal commands and migration advice.\nconst HOMEBREW_REMOVAL_INSTRUCTIONS: &str = r#\"To completely remove Homebrew from your system:\n\n- **Uninstall Homebrew and all packages:**\n   /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)\"\n\nFor a safer migration, consider using 'brew list' to inventory your packages before removal, then install equivalents via Nix.\"#;\n"
  },
  {
    "path": "crates/omnix-health/src/check/max_jobs.rs",
    "content": "use nix_rs::info;\nuse serde::{Deserialize, Serialize};\n\nuse crate::traits::*;\n\n/// Check that [nix_rs::config::NixConfig::max_jobs] is set to a good value.\n#[derive(Debug, Default, Serialize, Deserialize, Clone)]\n#[serde(default)]\npub struct MaxJobs {}\n\nimpl Checkable for MaxJobs {\n    fn check(\n        &self,\n        nix_info: &info::NixInfo,\n        _: Option<&nix_rs::flake::url::FlakeUrl>,\n    ) -> Vec<(&'static str, Check)> {\n        let max_jobs = nix_info.nix_config.max_jobs.value;\n        let check = Check {\n            title: \"Max Jobs\".to_string(),\n            info: format!(\"max-jobs = {}\", max_jobs),\n            result: if max_jobs > 1 {\n                CheckResult::Green\n            } else {\n                CheckResult::Red {\n                    msg: \"You are using only 1 CPU core for nix builds\".into(),\n                    suggestion: format!(\n                        \"Set `max-jobs = auto` in {}\",\n                        nix_info.nix_env.os.nix_config_label()\n                    ),\n                }\n            },\n            required: true,\n        };\n\n        vec![(\"max-jobs\", check)]\n    }\n}\n"
  },
  {
    "path": "crates/omnix-health/src/check/mod.rs",
    "content": "//! Individual Nix checks\npub mod caches;\npub mod direnv;\npub mod flake_enabled;\npub mod homebrew;\npub mod max_jobs;\npub mod nix_version;\npub mod rosetta;\npub mod shell;\npub mod trusted_users;\n"
  },
  {
    "path": "crates/omnix-health/src/check/nix_version.rs",
    "content": "use std::str::FromStr;\n\nuse nix_rs::version_spec::NixVersionReq;\n\nuse nix_rs::info;\nuse serde::{Deserialize, Serialize};\n\nuse crate::traits::*;\n\n/// Check that [nix_rs::version::NixVersion] is set to a good value.\n#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]\n#[serde(default, rename_all = \"kebab-case\")]\npub struct NixVersionCheck {\n    pub supported: NixVersionReq,\n}\n\nimpl Default for NixVersionCheck {\n    fn default() -> Self {\n        NixVersionCheck {\n            supported: NixVersionReq::from_str(\">=2.16.0\").unwrap(),\n        }\n    }\n}\n\nimpl Checkable for NixVersionCheck {\n    fn check(\n        &self,\n        nix_info: &info::NixInfo,\n        _: Option<&nix_rs::flake::url::FlakeUrl>,\n    ) -> Vec<(&'static str, Check)> {\n        let val = &nix_info.nix_version;\n\n        let is_supported = self.supported.specs.iter().all(|spec| spec.matches(val));\n\n        let supported_version_check = Check {\n            title: \"Nix Version is supported\".to_string(),\n            info: format!(\"nix version = {}\", val),\n            result: if is_supported {\n                CheckResult::Green\n            } else {\n                CheckResult::Red {\n                    msg: format!(\n                        \"Your Nix version ({}) doesn't satisfy the supported bounds: {}\",\n                        val, self.supported\n                    ),\n                    suggestion: \"To use a specific version of Nix, see <https://nixos.asia/en/howto/nix-package>\"\n                        .into(),\n                }\n            },\n            required: true,\n        };\n\n        vec![(\"supported-nix-versions\", supported_version_check)]\n    }\n}\n"
  },
  {
    "path": "crates/omnix-health/src/check/rosetta.rs",
    "content": "use nix_rs::{env::OS, info};\nuse serde::{Deserialize, Serialize};\n\nuse crate::traits::{Check, CheckResult, Checkable};\n\n/// Check if Nix is being run under rosetta emulation\n///\n/// Enabled only on ARM macs.\n#[derive(Debug, Serialize, Deserialize, Clone)]\n#[serde(default)]\npub struct Rosetta {\n    enable: bool,\n    required: bool,\n}\n\nimpl Default for Rosetta {\n    fn default() -> Self {\n        Self {\n            enable: true,\n            required: true,\n        }\n    }\n}\n\nimpl Checkable for Rosetta {\n    fn check(\n        &self,\n        nix_info: &info::NixInfo,\n        _: Option<&nix_rs::flake::url::FlakeUrl>,\n    ) -> Vec<(&'static str, Check)> {\n        let mut checks = vec![];\n        if let (true, Some(emulation)) = (self.enable, get_apple_emulation(&nix_info.nix_env.os)) {\n            let check = Check {\n                title: \"Rosetta Not Active\".to_string(),\n                info: format!(\"apple emulation = {:?}\", emulation),\n                result: if emulation {\n                    CheckResult::Red {\n                    msg: \"Rosetta emulation will slow down Nix builds\".to_string(),\n                    // NOTE: This check assumes that `omnix` was installed via `nix`, thus assuming `nix` is also translated using Rosetta.\n                    // Hence, the suggestion to re-install nix.\n                    suggestion: \"Disable Rosetta for your terminal (Right-click on your terminal icon in `Finder`, choose `Get Info` and un-check `Open using Rosetta`). Uninstall nix: <https://nixos.asia/en/gotchas/macos-upgrade>. And re-install for `aarch64-darwin`: <https://nixos.asia/en/install>\".to_string(),\n                }\n                } else {\n                    CheckResult::Green\n                },\n                required: self.required,\n            };\n            checks.push((\"rosetta\", check));\n        };\n        checks\n    }\n}\n\n/// Return [true] if the current binary is translated using Rosetta. Return None if not an ARM mac.\nfn get_apple_emulation(system: &OS) -> Option<bool> {\n    match system {\n        OS::MacOS {\n            nix_darwin: _,\n            arch: _,\n            proc_translated: is_proc_translated,\n        } => Some(*is_proc_translated),\n        _ => None,\n    }\n}\n"
  },
  {
    "path": "crates/omnix-health/src/check/shell.rs",
    "content": "use nix_rs::env::OS;\nuse serde::{Deserialize, Serialize};\nuse std::{\n    collections::HashMap,\n    hash::Hash,\n    path::{Path, PathBuf},\n};\n\nuse crate::traits::{Check, CheckResult, Checkable};\n\n#[derive(Debug, Serialize, Deserialize, Clone, Hash, Eq, PartialEq)]\npub struct ShellCheck {\n    pub(crate) enable: bool,\n    /// Whether to produce [Check::required] checks\n    pub(crate) required: bool,\n}\n\nimpl Default for ShellCheck {\n    fn default() -> Self {\n        Self {\n            enable: true,\n            required: false,\n        }\n    }\n}\n\nimpl Checkable for ShellCheck {\n    fn check(\n        &self,\n        nix_info: &nix_rs::info::NixInfo,\n        _flake: Option<&nix_rs::flake::url::FlakeUrl>,\n    ) -> Vec<(&'static str, Check)> {\n        if !self.enable {\n            return vec![];\n        }\n        let os = &nix_info.nix_env.os;\n        let user_shell_env = match CurrentUserShellEnv::new(os) {\n            Ok(shell) => shell,\n            Err(err) => {\n                tracing::error!(\"Skipping shell dotfile check! {:?}\", err);\n                if self.required {\n                    panic!(\"Unable to determine user's shell environment (see above)\");\n                } else {\n                    tracing::warn!(\"Skipping shell dotfile check! (see above)\");\n                    return vec![];\n                }\n            }\n        };\n\n        // Iterate over each dotfile and check if it is managed by Nix\n        let mut managed: HashMap<String, PathBuf> = HashMap::new();\n        let mut unmanaged: HashMap<String, PathBuf> = HashMap::new();\n        for (name, path) in user_shell_env.dotfiles {\n            if super::direnv::is_path_in_nix_store(&path) {\n                managed.insert(name, path.clone());\n            } else {\n                unmanaged.insert(name, path.clone());\n            }\n        }\n\n        let title = \"Shell dotfiles\".to_string();\n        let info = format!(\n            \"Shell={:?}; HOME={:?}; Managed: {:?}; Unmanaged: {:?}\",\n            user_shell_env.shell, user_shell_env.home, managed, unmanaged\n        );\n        let result = if !managed.is_empty() && unmanaged.is_empty() {\n            // If *all* dotfiles are managed, then we are good\n            CheckResult::Green\n        } else {\n            CheckResult::Red {\n                msg: format!(\"Default Shell: {:?} is not managed by Nix\", user_shell_env.shell),\n                    suggestion: \"You can use `home-manager` to manage shell configuration. See <https://github.com/juspay/nixos-unified-template>\".to_string(),\n            }\n        };\n        let check = Check {\n            title,\n            info,\n            result,\n            required: self.required,\n        };\n\n        vec![(\"shell\", check)]\n    }\n}\n\n/// The shell environment of the current user\nstruct CurrentUserShellEnv {\n    /// The user's home directory\n    home: PathBuf,\n    /// Current shell\n    shell: Shell,\n    /// *Absolute* paths to the dotfiles\n    dotfiles: HashMap<String, PathBuf>,\n}\n\nimpl CurrentUserShellEnv {\n    /// Get the current user's shell environment\n    fn new(os: &OS) -> Result<Self, ShellError> {\n        let home = PathBuf::from(std::env::var(\"HOME\")?);\n        let shell = Shell::current_shell()?;\n        let dotfiles = shell.get_dotfiles(os, &home)?;\n        let v = CurrentUserShellEnv {\n            home,\n            shell,\n            dotfiles,\n        };\n        Ok(v)\n    }\n}\n\n#[derive(thiserror::Error, Debug)]\nenum ShellError {\n    #[error(\"IO error: {0}\")]\n    Io(#[from] std::io::Error),\n\n    #[error(\"Environment variable error: {0}\")]\n    Var(#[from] std::env::VarError),\n\n    #[error(\"Bad $SHELL value\")]\n    BadShellPath,\n\n    #[error(\"Unsupported shell. Please file an issue at <https://github.com/juspay/omnix/issues>\")]\n    UnsupportedShell,\n}\n\n/// An Unix shell\n#[derive(Debug, Serialize, Deserialize, Clone, Hash, Eq, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\nenum Shell {\n    Zsh,\n    Bash,\n    Nushell,\n}\n\nimpl Shell {\n    /// Returns the user's current [Shell]\n    fn current_shell() -> Result<Self, ShellError> {\n        let shell_path = PathBuf::from(std::env::var(\"SHELL\")?);\n        Self::from_path(shell_path)\n    }\n\n    /// Lookup [Shell] from the given executable path\n    /// For example if path is `/bin/zsh`, it would return `Zsh`\n    fn from_path(exe_path: PathBuf) -> Result<Self, ShellError> {\n        let shell_name = exe_path\n            .file_name()\n            .ok_or(ShellError::BadShellPath)?\n            .to_string_lossy();\n\n        match shell_name.as_ref() {\n            \"zsh\" => Ok(Shell::Zsh),\n            \"bash\" => Ok(Shell::Bash),\n            \"nu\" => Ok(Shell::Nushell),\n            _ => Err(ShellError::UnsupportedShell),\n        }\n    }\n\n    /// Get shell dotfiles\n    fn dotfile_names(&self, os: &OS) -> Vec<String> {\n        match &self {\n            Shell::Zsh => vec![\n                \".zshrc\".into(),\n                \".zshenv\".into(),\n                \".zprofile\".into(),\n                \".zlogin\".into(),\n                \".zlogout\".into(),\n            ],\n            Shell::Bash => vec![\".bashrc\".into(), \".bash_profile\".into(), \".profile\".into()],\n            Shell::Nushell => {\n                let base = match os {\n                    // https://www.nushell.sh/book/configuration.html#configuration-overview\n                    OS::MacOS { .. } => \"Library/Application Support/nushell\",\n                    _ => \".config/nushell\",\n                };\n                [\"env.nu\", \"config.nu\", \"login.nu\"]\n                    .iter()\n                    .map(|f| format!(\"{}/{}\", base, f))\n                    .collect()\n            }\n        }\n    }\n\n    /// Get the currently existing dotfiles under $HOME\n    ///\n    /// Returned paths will be absolute (i.e., symlinks are resolved).\n    fn get_dotfiles(&self, os: &OS, home_dir: &Path) -> std::io::Result<HashMap<String, PathBuf>> {\n        let mut paths = HashMap::new();\n        for dotfile in self.dotfile_names(os) {\n            match std::fs::canonicalize(home_dir.join(&dotfile)) {\n                Ok(path) => {\n                    paths.insert(dotfile, path);\n                }\n                Err(err) if err.kind() == std::io::ErrorKind::NotFound => {\n                    // If file not found, skip\n                }\n                Err(err) => return Err(err),\n            }\n        }\n        Ok(paths)\n    }\n}\n"
  },
  {
    "path": "crates/omnix-health/src/check/trusted_users.rs",
    "content": "use std::collections::HashSet;\n\nuse nix_rs::config::TrustedUserValue;\nuse serde::{Deserialize, Serialize};\n\nuse crate::traits::*;\n\n/// Check that [nix_rs::config::NixConfig::trusted_users] is set to a good value.\n#[derive(Debug, Default, Serialize, Deserialize, Clone)]\n#[serde(default)]\npub struct TrustedUsers {\n    /// This check is disabled by default due to security concerns\n    /// See https://github.com/juspay/omnix/issues/409\n    pub(crate) enable: bool,\n}\n\nimpl Checkable for TrustedUsers {\n    fn check(\n        &self,\n        nix_info: &nix_rs::info::NixInfo,\n        _: Option<&nix_rs::flake::url::FlakeUrl>,\n    ) -> Vec<(&'static str, Check)> {\n        if !self.enable {\n            return vec![];\n        }\n        let result = if is_current_user_trusted(nix_info) {\n            CheckResult::Green\n        } else {\n            let current_user = &nix_info.nix_env.current_user;\n            let msg = format!(\"User '{}' not present in trusted_users\", current_user);\n            let suggestion = match nix_info.nix_env.os.nix_system_config_label() {\n                Some(conf_label) => format!(\n                    r#\"Add `nix.trustedUsers = [ \"root\" \"{}\" ];` to your {}\"#,\n                    current_user, conf_label,\n                ),\n                None => format!(\n                    r#\"Set `trusted-users = root {}` in /etc/nix/nix.conf and then restart the Nix daemon using `sudo pkill nix-daemon`\"#,\n                    current_user\n                ),\n            };\n            CheckResult::Red { msg, suggestion }\n        };\n        let check = Check {\n            title: \"Trusted Users\".to_string(),\n            info: format!(\n                \"trusted-users = {}\",\n                TrustedUserValue::display_original(&nix_info.nix_config.trusted_users.value)\n            ),\n            result,\n            required: true,\n        };\n\n        vec![(\"trusted-users\", check)]\n    }\n}\n\n/// Check that [nix_rs::config::NixConfig::trusted_users] is set to a good\n/// value such that the current user is trusted.\nfn is_current_user_trusted(nix_info: &nix_rs::info::NixInfo) -> bool {\n    let current_user = &nix_info.nix_env.current_user;\n    let current_user_groups: HashSet<&String> =\n        nix_info.nix_env.current_user_groups.iter().collect();\n    nix_info\n        .nix_config\n        .trusted_users\n        .value\n        .iter()\n        .any(|x| match x {\n            TrustedUserValue::Group(x) => current_user_groups.contains(&x),\n            TrustedUserValue::User(x) => x == current_user,\n            TrustedUserValue::All => true,\n        })\n}\n"
  },
  {
    "path": "crates/omnix-health/src/json.rs",
    "content": "//! JSON output schema for health checks\nuse crate::traits::Check;\nuse anyhow::Context;\nuse bytesize::ByteSize;\nuse nix_rs::{\n    env::{NixInstaller, OS},\n    flake::system::System,\n    info::NixInfo,\n};\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\n\n/// Entire JSON health check output\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct HealthOutput {\n    /// Map of check names to their results\n    pub checks: HashMap<String, Check>,\n    /// System environment information\n    pub info: HealthEnvInfo,\n}\n\nimpl HealthOutput {\n    pub async fn get(checks: Vec<(&'static str, Check)>) -> anyhow::Result<Self> {\n        Ok(Self {\n            checks: checks\n                .into_iter()\n                .map(|(k, v)| (k.to_string(), v))\n                .collect(),\n            info: HealthEnvInfo::get().await?,\n        })\n    }\n}\n\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct HealthEnvInfo {\n    nix_installer: NixInstaller,\n    system: System,\n    os: OS,\n    total_memory: ByteSize,\n    total_disk_space: ByteSize,\n}\n\nimpl HealthEnvInfo {\n    /// Get system environment information\n    ///\n    /// Returns error if [NixInfo] cannot be retrieved\n    pub async fn get() -> anyhow::Result<Self> {\n        let nix_info = NixInfo::get()\n            .await\n            .as_ref()\n            .context(\"Unable to gather nix info\")?;\n\n        Ok(Self {\n            nix_installer: nix_info.nix_env.installer.clone(),\n            system: nix_info.nix_config.system.value.clone(),\n            os: nix_info.nix_env.os.clone(),\n            total_memory: nix_info.nix_env.total_memory,\n            total_disk_space: nix_info.nix_env.total_disk_space,\n        })\n    }\n}\n"
  },
  {
    "path": "crates/omnix-health/src/lib.rs",
    "content": "//! Health checks for the user's Nix install\n\npub mod check;\npub mod json;\npub mod report;\npub mod traits;\n\nuse anyhow::Context;\nuse check::shell::ShellCheck;\nuse colored::Colorize;\n\nuse check::direnv::Direnv;\nuse check::homebrew::Homebrew;\nuse json::HealthOutput;\nuse nix_rs::command::NixCmd;\nuse nix_rs::env::OS;\nuse nix_rs::flake::url::FlakeUrl;\nuse nix_rs::info::NixInfo;\nuse omnix_common::config::{OmConfig, OmConfigError};\nuse omnix_common::markdown::render_markdown;\nuse serde::{Deserialize, Serialize};\nuse tracing::instrument;\nuse traits::Check;\n\nuse self::check::{\n    caches::Caches, flake_enabled::FlakeEnabled, max_jobs::MaxJobs, nix_version::NixVersionCheck,\n    rosetta::Rosetta, trusted_users::TrustedUsers,\n};\n\n/// Nix Health check of user's install\n///\n/// Each check field is expected to implement [traits::Checkable].\n#[derive(Debug, Default, Serialize, Deserialize, Clone)]\n#[serde(default, rename_all = \"kebab-case\")]\npub struct NixHealth {\n    pub flake_enabled: FlakeEnabled,\n    pub nix_version: NixVersionCheck,\n    pub rosetta: Rosetta,\n    pub max_jobs: MaxJobs,\n    pub trusted_users: TrustedUsers,\n    pub caches: Caches,\n    pub direnv: Direnv,\n    pub homebrew: Homebrew,\n    pub shell: ShellCheck,\n}\n\n/// Convert [NixHealth] into a generic [Vec] of checks\nimpl<'a> IntoIterator for &'a NixHealth {\n    type Item = &'a dyn traits::Checkable;\n    type IntoIter = std::vec::IntoIter<Self::Item>;\n\n    /// Return an iterator to iterate on the fields of [NixHealth]\n    fn into_iter(self) -> Self::IntoIter {\n        let items: Vec<Self::Item> = vec![\n            &self.flake_enabled,\n            &self.nix_version,\n            &self.rosetta,\n            &self.max_jobs,\n            &self.trusted_users,\n            &self.caches,\n            &self.direnv,\n            &self.homebrew,\n            &self.shell,\n        ];\n        items.into_iter()\n    }\n}\n\nimpl NixHealth {\n    /// Create [NixHealth] using configuration from the given flake\n    ///\n    /// Fallback to using the default health check config if the flake doesn't\n    /// override it.\n    pub fn from_om_config(om_config: &OmConfig) -> Result<Self, OmConfigError> {\n        let (cfg, _rest) = om_config.get_sub_config_under::<Self>(\"health\")?;\n        Ok(cfg.clone())\n    }\n\n    /// Run all checks and collect the results\n    #[instrument(skip_all)]\n    pub fn run_all_checks(\n        &self,\n        nix_info: &NixInfo,\n        flake_url: Option<FlakeUrl>,\n    ) -> Vec<(&'static str, Check)> {\n        self.into_iter()\n            .flat_map(|c| c.check(nix_info, flake_url.as_ref()))\n            .collect()\n    }\n\n    pub async fn print_report_returning_exit_code(\n        checks: &Vec<(&'static str, Check)>,\n        json_only: bool,\n    ) -> anyhow::Result<i32> {\n        let mut res = AllChecksResult::new();\n        for (_, check) in checks {\n            if !json_only {\n                check.tracing_log().await?;\n            }\n            if !check.result.green() {\n                res.register_failure(check.required);\n            };\n        }\n\n        let code = res.report();\n\n        if json_only {\n            let json = HealthOutput::get(checks.to_vec()).await?;\n            println!(\"{}\", serde_json::to_string(&json)?);\n        }\n        Ok(code)\n    }\n\n    pub fn schema() -> Result<String, serde_json::Error> {\n        serde_json::to_string_pretty(&NixHealth::default())\n    }\n}\n\n/// Run all health checks, optionally using the given flake's configuration\npub async fn run_all_checks_with(\n    nixcmd: &NixCmd,\n    flake_url: Option<FlakeUrl>,\n    json_only: bool,\n) -> anyhow::Result<Vec<(&'static str, Check)>> {\n    let nix_info = NixInfo::get()\n        .await\n        .as_ref()\n        .with_context(|| \"Unable to gather nix info\")?;\n\n    let health: NixHealth = match flake_url.as_ref() {\n        Some(flake_url) => {\n            let om_config = OmConfig::get(nixcmd, flake_url).await?;\n            NixHealth::from_om_config(&om_config)\n        }\n        None => Ok(NixHealth::default()),\n    }?;\n\n    tracing::info!(\n        \"🩺️ Checking the health of your Nix setup (flake: '{}')\",\n        match flake_url.as_ref() {\n            Some(url) => url.to_string(),\n            None => \"N/A\".to_string(),\n        }\n    );\n\n    if !json_only {\n        print_info_banner(flake_url.as_ref(), nix_info).await?;\n    }\n\n    let checks = health.run_all_checks(nix_info, flake_url);\n    Ok(checks)\n}\n\nasync fn print_info_banner(flake_url: Option<&FlakeUrl>, nix_info: &NixInfo) -> anyhow::Result<()> {\n    let pwd = std::env::current_dir()?;\n\n    let mut table = String::from(\"| Property | Value |\\n|----------|-------|\\n\");\n    table.push_str(&format!(\n        \"| Flake | {} |\\n\",\n        match flake_url {\n            Some(url) => url.to_string(),\n            None => \"N/A\".to_string(),\n        }\n    ));\n    table.push_str(&format!(\n        \"| System | {} |\\n\",\n        nix_info.nix_config.system.value\n    ));\n    table.push_str(&format!(\"| OS | {} |\\n\", nix_info.nix_env.os));\n    if nix_info.nix_env.os != OS::NixOS {\n        table.push_str(&format!(\n            \"| Nix installer | {} |\\n\",\n            nix_info.nix_env.installer\n        ));\n    }\n    table.push_str(&format!(\"| RAM | {:?} |\\n\", nix_info.nix_env.total_memory));\n    table.push_str(&format!(\n        \"| Disk Space | {:?} |\",\n        nix_info.nix_env.total_disk_space\n    ));\n\n    tracing::info!(\"{}\", render_markdown(&pwd, &table).await?);\n    Ok(())\n}\n\n/// A convenient type to aggregate check failures, and summary report at end.\nenum AllChecksResult {\n    Pass,\n    PassSomeFail,\n    Fail,\n}\n\nimpl AllChecksResult {\n    fn new() -> Self {\n        AllChecksResult::Pass\n    }\n\n    fn register_failure(&mut self, required: bool) {\n        if required {\n            *self = AllChecksResult::Fail;\n        } else if matches!(self, AllChecksResult::Pass) {\n            *self = AllChecksResult::PassSomeFail;\n        }\n    }\n\n    /// Print a summary report of the checks and return the exit code\n    fn report(self) -> i32 {\n        match self {\n            AllChecksResult::Pass => {\n                tracing::info!(\"{}\", \"✅ All checks passed\".green().bold());\n                0\n            }\n            AllChecksResult::PassSomeFail => {\n                tracing::warn!(\n                    \"{}, {}\",\n                    \"✅ Required checks passed\".green().bold(),\n                    \"but some non-required checks failed\".yellow().bold()\n                );\n                0\n            }\n            AllChecksResult::Fail => {\n                tracing::error!(\"{}\", \"❌ Some required checks failed\".red().bold());\n                1\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::str::FromStr;\n\n    use nix_rs::version_spec::NixVersionReq;\n\n    use crate::check::{caches::Caches, nix_version::NixVersionCheck};\n\n    #[test]\n    fn test_json_deserialize_empty() {\n        let json = r#\"{}\"#;\n        let v: super::NixHealth = serde_json::from_str(json).unwrap();\n        assert_eq!(v.nix_version, NixVersionCheck::default());\n        assert_eq!(v.caches, Caches::default());\n        println!(\"{:?}\", v);\n    }\n\n    #[test]\n    fn test_json_deserialize_nix_version() {\n        let json = r#\"{ \"nix-version\": { \"supported\": \">=2.17.0\" } }\"#;\n        let v: super::NixHealth = serde_json::from_str(json).unwrap();\n        assert_eq!(\n            v.nix_version.supported,\n            NixVersionReq::from_str(\">=2.17.0\").unwrap()\n        );\n        assert_eq!(v.caches, Caches::default());\n    }\n\n    #[test]\n    fn test_json_deserialize_caches() {\n        let json = r#\"{ \"caches\": { \"required\": [\"https://foo.cachix.org\"] } }\"#;\n        let v: super::NixHealth = serde_json::from_str(json).unwrap();\n        assert_eq!(\n            v.caches.required,\n            vec![url::Url::parse(\"https://foo.cachix.org\").unwrap()]\n        );\n        assert_eq!(v.nix_version, NixVersionCheck::default());\n    }\n}\n"
  },
  {
    "path": "crates/omnix-health/src/report.rs",
    "content": "use serde::{Deserialize, Serialize};\n\n/// Health report\n///\n/// If you just want the binary indicator, use `Report<NoDetails>` (see\n/// [NoDetails]). If you want the report with details regarding the problem, use\n/// `Report<WithDetails>` (see [WithDetails]).\n#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize, Clone)]\npub enum Report<T> {\n    /// Green means everything is fine\n    Green,\n    /// Red means something is wrong. `T` holds information about what's wrong.\n    Red(T),\n}\n\n#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize, Clone)]\npub struct NoDetails;\n\nimpl<R> Report<R> {\n    pub fn is_green(&self) -> bool {\n        match self {\n            Report::Green => true,\n            Report::Red(_) => false,\n        }\n    }\n\n    pub fn is_red(&self) -> bool {\n        !self.is_green()\n    }\n}\n\n/// Details regarding a failed report\n#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize, Clone)]\npub struct WithDetails {\n    /// A short message describing the problem\n    pub msg: String,\n    /// A suggestion for how to fix the problem\n    pub suggestion: String,\n} // TODO: Should this be Markdown?\n\nimpl Report<WithDetails> {\n    /// Return the report without the details\n    pub fn without_details(&self) -> Report<NoDetails> {\n        match self {\n            Report::Green => Report::Green,\n            Report::Red(_) => Report::Red(NoDetails),\n        }\n    }\n    /// Return the problem details if there is one.\n    pub fn get_red_details(&self) -> Option<WithDetails> {\n        match self {\n            Report::Green => None,\n            Report::Red(details) => Some(details.clone()),\n        }\n    }\n}\n"
  },
  {
    "path": "crates/omnix-health/src/traits.rs",
    "content": "use colored::Colorize;\nuse serde::{Deserialize, Serialize};\n\n/// Types that can do specific \"health check\" for Nix\npub trait Checkable {\n    /// Run and create the health check\n    ///\n    /// NOTE: Some checks may perform impure actions (IO, etc.). Returning an\n    /// empty vector indicates that the check is skipped on this environment.\n    fn check(\n        &self,\n        nix_info: &nix_rs::info::NixInfo,\n        // The flake against which the check is being run\n        //\n        // If None, the check is run against the current environment, with no\n        // specific configuration from a flake.\n        flake: Option<&nix_rs::flake::url::FlakeUrl>,\n    ) -> Vec<(&'static str, Check)>;\n}\n\n/// A health check\n#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]\npub struct Check {\n    /// A user-facing title of this check\n    ///\n    /// This value is expected to be unique across all checks.\n    pub title: String,\n\n    /// The user-facing information used to conduct this check\n    /// TODO: Use Markdown\n    pub info: String,\n\n    /// The result of running this check\n    pub result: CheckResult,\n\n    /// Whether this check is mandatory\n    ///\n    /// Failures are considered non-critical if this is false.\n    pub required: bool,\n}\n\nimpl Check {\n    /// Log the results using tracing crate\n    pub async fn tracing_log(&self) -> anyhow::Result<()> {\n        let pwd = std::env::current_dir()?;\n        use omnix_common::markdown::render_markdown;\n        match &self.result {\n            CheckResult::Green => {\n                tracing::info!(\"✅ {}\", self.title.green().bold());\n                tracing::info!(\"{}\", render_markdown(&pwd, &self.info).await?.dimmed());\n            }\n            CheckResult::Red { msg, suggestion } => {\n                let solution = render_markdown(\n                    &pwd,\n                    &format!(\"**Problem**: {}\\\\\\n**Fix**:     {}\\n\", msg, suggestion),\n                )\n                .await?;\n                if self.required {\n                    tracing::error!(\n                        \"❌ {}\",\n                        render_markdown(&pwd, &self.title).await?.red().bold()\n                    );\n                    tracing::error!(\"{}\", render_markdown(&pwd, &self.info).await?.dimmed());\n                    tracing::error!(\"{}\", solution);\n                } else {\n                    tracing::warn!(\n                        \"🟧 {}\",\n                        render_markdown(&pwd, &self.title).await?.yellow().bold()\n                    );\n                    tracing::warn!(\"{}\", render_markdown(&pwd, &self.info).await?.dimmed());\n                    tracing::warn!(\"{}\", solution);\n                }\n            }\n        }\n        Ok(())\n    }\n}\n\n/// The result of a health [Check]\n#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]\npub enum CheckResult {\n    /// The check passed\n    Green,\n\n    /// The check failed\n    Red {\n        /// TODO: Use markdown\n        msg: String,\n        /// TODO: Use markdown\n        suggestion: String,\n    },\n}\n\nimpl CheckResult {\n    /// When the check is green (ie., healthy)\n    pub fn green(&self) -> bool {\n        matches!(self, Self::Green)\n    }\n}\n"
  },
  {
    "path": "crates/omnix-init/Cargo.toml",
    "content": "[package]\nauthors = [\"Sridhar Ratnakumar <srid@srid.ca>\"]\nedition = \"2021\"\n# If you change the name here, you must also do it in flake.nix (and run `cargo generate-lockfile` afterwards)\nname = \"omnix-init\"\nversion = \"1.3.2\"\nrepository = \"https://github.com/juspay/omnix\"\ndescription = \"Enriched Nix flake templates\"\nlicense = \"Apache-2.0\"\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nanyhow = { workspace = true }\nassert_fs = \"1\"\nassert_matches = \"1.5\"\ncolored = { workspace = true }\nconsole = { workspace = true }\nglobset = { workspace = true }\ninquire = { workspace = true }\nitertools = { workspace = true }\nlazy_static = { workspace = true }\nnix_rs = { workspace = true }\nserde = { workspace = true }\nserde_json = { workspace = true }\nthiserror = { workspace = true }\ntokio = { workspace = true }\ntracing = { workspace = true }\nomnix-common = { workspace = true }\n"
  },
  {
    "path": "crates/omnix-init/crate.nix",
    "content": "{\n  autoWire = [ ];\n  crane.args = { };\n}\n"
  },
  {
    "path": "crates/omnix-init/registry/flake.nix",
    "content": "# `om init` registry's canonical source\n#\n# Get JSON using:\n# nix eval --json .#registry | jq\n{\n  inputs = {\n    haskell-flake.url = \"github:srid/haskell-flake\";\n    haskell-flake.flake = false;\n\n    haskell-template.url = \"github:srid/haskell-template\";\n    haskell-template.flake = false;\n\n    rust-nix-template.url = \"github:srid/rust-nix-template\";\n    rust-nix-template.flake = false;\n\n    nixos-unified-template.url = \"github:juspay/nixos-unified-template\";\n    nixos-unified-template.flake = false;\n  };\n\n  outputs = inputs: {\n    registry =\n      builtins.mapAttrs\n        (k: v: v.outPath)\n        (builtins.removeAttrs inputs [ \"self\" ]);\n  };\n}\n"
  },
  {
    "path": "crates/omnix-init/src/action.rs",
    "content": "use anyhow::Context;\nuse globset::{Glob, GlobSetBuilder};\nuse itertools::Itertools;\nuse serde::Deserialize;\nuse std::cmp::Ordering;\nuse std::fmt::{self, Display, Formatter};\nuse std::path::Path;\nuse tokio::fs;\n\n/// The action to perform on a template\n#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]\n#[serde(untagged)]\npub enum Action {\n    /// Replace 'placeholder' with 'value' if it exists\n    Replace {\n        /// The text to replace.\n        placeholder: String,\n        /// The text to replace it with.\n        #[serde(default)]\n        value: Option<String>,\n    },\n    /// Delete given paths if 'value' is false\n    Retain {\n        /// The glob patterns to retain or delete\n        paths: Vec<Glob>,\n        /// Whether to retain or delete\n        #[serde(default)]\n        value: Option<bool>,\n    },\n}\n\nimpl Display for Action {\n    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {\n        match self {\n            Action::Replace { placeholder, value } => match value {\n                Some(value) => write!(f, \"replace [{} => {}]\", placeholder, value),\n                None => write!(f, \"replace [disabled]\"),\n            },\n            Action::Retain { paths, value } => match value {\n                Some(false) => {\n                    let paths = paths.iter().map(|p| p.to_string()).join(\", \");\n                    write!(f, \"prune [{}]\", paths)\n                }\n                _ => write!(f, \"prune [disabled]\"),\n            },\n        }\n    }\n}\n\nimpl Action {\n    /// Whether there is a current value in this action\n    pub fn has_value(&self) -> bool {\n        match self {\n            Action::Replace { value, .. } => value.is_some(),\n            Action::Retain { value, .. } => value.is_some(),\n        }\n    }\n\n    /// Apply the [Action] to the given directory\n    pub async fn apply(&self, out_dir: &Path) -> anyhow::Result<()> {\n        match &self {\n            Action::Replace { placeholder, value } => {\n                if let Some(value) = value.as_ref() {\n                    let files = omnix_common::fs::find_paths(out_dir).await?;\n\n                    // Process files in reverse order, such that we replace in\n                    // files *before* their ancestor directories get renamed.\n                    for file in files.iter().sorted().rev() {\n                        let file_path = &out_dir.join(file);\n\n                        // Replace in content of files\n                        if file_path.is_file() {\n                            let content =\n                                fs::read_to_string(&file_path).await.with_context(|| {\n                                    format!(\"Unable to read file: {:?}\", &file_path)\n                                })?;\n                            if content.contains(placeholder) {\n                                tracing::info!(\"   ✍️ {}\", file.to_string_lossy());\n                                let content = content.replace(placeholder, value);\n                                fs::write(file_path, content).await?;\n                            }\n                        }\n\n                        // Rename path if necessary\n                        if let Some(file_name) = file.file_name().map(|f| f.to_string_lossy()) {\n                            if file_name.contains(placeholder) {\n                                let new_name = file_name.replace(placeholder, value);\n                                let new_path = &file_path.with_file_name(&new_name);\n                                if file != new_path {\n                                    tracing::info!(\"   ✏️ {} => {}\", file.display(), &new_name,);\n                                    fs::rename(file_path, new_path).await?;\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            Action::Retain { paths, value } => {\n                if *value == Some(false) {\n                    // Get files matching\n                    let files = omnix_common::fs::find_paths(out_dir).await?;\n                    let set = build_glob_set(paths)?;\n                    let files_to_delete = files\n                        .iter()\n                        .filter(|file| set.is_match(file))\n                        .collect::<Vec<_>>();\n                    if files_to_delete.is_empty() {\n                        anyhow::bail!(\"No paths matched in {:?}\", files);\n                    };\n                    // Iterating in reverse-sorted order ensures that children gets deleted before their parent folders.\n                    for file in files_to_delete.iter().sorted().rev() {\n                        let path = out_dir.join(file);\n                        tracing::info!(\"   ❌ {}\", file.display());\n                        omnix_common::fs::remove_all(path).await?;\n                    }\n                }\n            }\n        }\n        Ok(())\n    }\n}\n\n// Combine multiple glob patterns into a single set\nfn build_glob_set(globs: &[Glob]) -> anyhow::Result<globset::GlobSet> {\n    let mut builder = GlobSetBuilder::new();\n    for g in globs.iter() {\n        builder.add(g.clone());\n    }\n    Ok(builder.build()?)\n}\n\n// Implement Ord such that 'Retain' appears before 'Replace' in the list\n// Because, Retain will delete files, which affect the Replace actions.\nimpl Ord for Action {\n    fn cmp(&self, other: &Self) -> Ordering {\n        match (self, other) {\n            (Action::Retain { .. }, Action::Replace { .. }) => Ordering::Less,\n            (Action::Replace { .. }, Action::Retain { .. }) => Ordering::Greater,\n            _ => Ordering::Equal,\n        }\n    }\n}\n\nimpl PartialOrd for Action {\n    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {\n        Some(self.cmp(other))\n    }\n}\n"
  },
  {
    "path": "crates/omnix-init/src/config.rs",
    "content": "use std::fmt::{self, Display, Formatter};\n\nuse colored::Colorize;\nuse nix_rs::{command::NixCmd, flake::url::FlakeUrl};\nuse omnix_common::config::OmConfig;\n\nuse crate::template::Template;\n\n/// A named [Template] associated with a [FlakeUrl]\n#[derive(Debug, Clone)]\npub struct FlakeTemplate<'a> {\n    pub flake: &'a FlakeUrl,\n    pub template_name: String,\n    pub template: Template,\n}\n\n// This instance is used during user prompting.\nimpl Display for FlakeTemplate<'_> {\n    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {\n        write!(\n            f,\n            \"{:<15} {} {}\",\n            self.template_name,\n            format!(\"[{}]\", self.flake).dimmed(),\n            self.template\n                .template\n                .description\n                .as_ref()\n                .unwrap_or(&\"\".to_string())\n        )\n    }\n}\n\n/// Load templates from the given flake\npub async fn load_templates<'a>(\n    nixcmd: &'a NixCmd,\n    url: &'a FlakeUrl,\n) -> anyhow::Result<Vec<FlakeTemplate<'a>>> {\n    let om_config = OmConfig::get(nixcmd, url).await?;\n\n    let templates = om_config\n        .config\n        .get::<Template>(\"templates\")?\n        .ok_or_else(|| anyhow::anyhow!(\"No templates found\"))?;\n\n    Ok(templates\n        .into_iter()\n        .map(|(k, v)| FlakeTemplate {\n            flake: url,\n            template_name: k,\n            template: v,\n        })\n        .collect())\n}\n"
  },
  {
    "path": "crates/omnix-init/src/core.rs",
    "content": "use std::{collections::HashMap, path::Path};\n\nuse crate::config::{load_templates, FlakeTemplate};\nuse anyhow::Context;\nuse nix_rs::{\n    command::NixCmd,\n    flake::{system::System, url::FlakeUrl},\n};\nuse omnix_common::markdown::print_markdown;\nuse serde_json::Value;\n\npub async fn select_from_registry(nixcmd: &NixCmd) -> anyhow::Result<FlakeUrl> {\n    let builtin_registry = crate::registry::get(nixcmd).await.as_ref()?;\n    // Prompt the user to select a flake from the registry\n    let available: Vec<String> = builtin_registry.0.keys().cloned().collect();\n    let name = inquire::Select::new(\"Select a flake\", available).prompt()?;\n    builtin_registry\n        .0\n        .get(&name)\n        .cloned()\n        .ok_or(anyhow::anyhow!(\"Flake not found in builtin registry\"))\n}\n\npub async fn run_tests(\n    nixcmd: &NixCmd,\n    current_system: &System,\n    flake: &FlakeUrl,\n) -> anyhow::Result<()> {\n    let templates = load_templates(nixcmd, flake).await?;\n    for template in templates.iter() {\n        tracing::info!(\"🕍 Testing template: {}#{}\", flake, template.template_name);\n        for (name, test) in template.template.tests.iter() {\n            if test.can_run_on(current_system) {\n                tracing::info!(\"🧪 Running test: {} (on {})\", name, current_system);\n                test.run_test(\n                    &flake.with_attr(&format!(\"{}.{}\", template.template_name, name)),\n                    template,\n                )\n                .await?;\n            } else {\n                tracing::info!(\n                    \"⚠️ Skipping test: {} (cannot run on {})\",\n                    name,\n                    current_system\n                );\n            }\n        }\n    }\n    Ok(())\n}\n\n/// Initialize a template at the given path\n///\n/// # Arguments\n/// - `path` - The path to initialize the template at\n/// - `name` - The name of the template to initialize\n/// - `default_params` - The default parameter values to use\n/// - `non_interactive` - Whether to disable user prompts (all params must have values set)\npub async fn run(\n    nixcmd: &NixCmd,\n    path: &Path,\n    flake: &FlakeUrl,\n    default_params: &HashMap<String, Value>,\n    non_interactive: bool,\n) -> anyhow::Result<()> {\n    let templates = load_templates(nixcmd, flake).await?;\n    // Prompt the user to select a template\n    let mut template: FlakeTemplate = if let Some(attr) = flake.get_attr().0 {\n        templates\n            .iter()\n            .find(|t| t.template_name == attr)\n            .cloned()\n            .with_context(|| \"Template not found\")?\n    } else if templates.len() < 2 {\n        if let Some(first) = templates.first() {\n            tracing::info!(\n                \"Automatically choosing the one template available: {}\",\n                first.template_name\n            );\n            first.clone()\n        } else {\n            return Err(anyhow::anyhow!(\"No templates available\"));\n        }\n    } else if non_interactive {\n        return Err(\n              anyhow::anyhow!(\"Non-interactive mode requires exactly one template to be available; but {} are available. Explicit specify it in flake URL.\",\n              templates.len()));\n    } else {\n        let select = inquire::Select::new(\"Select a template\", templates);\n        select.prompt()?.clone()\n    };\n\n    template.template.set_param_values(default_params);\n\n    if non_interactive {\n        for param in template.template.params.iter() {\n            if !param.action.has_value() {\n                return Err(anyhow::anyhow!(\n                    \"Non-interactive mode requires all parameters to be set; but {} is missing\",\n                    param.name\n                ));\n            }\n        }\n    } else {\n        for param in template.template.params.iter_mut() {\n            param.set_value_by_prompting()?;\n        }\n    }\n\n    initialize_template(path, &template).await?;\n    Ok(())\n}\n\nasync fn initialize_template(path: &Path, template: &FlakeTemplate<'_>) -> anyhow::Result<()> {\n    tracing::info!(\"Initializing template at {}\", path.display());\n    let path = template\n        .template\n        .scaffold_at(path)\n        .await\n        .with_context(|| \"Unable to scaffold\")?;\n    eprintln!();\n    print_markdown(\n        &path,\n        &format!(\"## 🥳 Initialized template at `{}`\", path.display()),\n    )\n    .await?;\n\n    if let Some(welcome_text) = template.template.template.welcome_text.as_ref() {\n        eprintln!();\n        print_markdown(&path, welcome_text).await?;\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "crates/omnix-init/src/lib.rs",
    "content": "#[macro_use]\nextern crate assert_matches;\npub mod action;\npub mod config;\npub mod core;\npub mod param;\npub mod registry;\npub mod template;\npub mod test;\n"
  },
  {
    "path": "crates/omnix-init/src/param.rs",
    "content": "use std::fmt::{self, Display, Formatter};\n\nuse serde::Deserialize;\nuse serde_json::Value;\n\nuse crate::action::Action;\n\n/// A template parameter that allows dynamically initializing it.\n#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]\npub struct Param {\n    pub name: String,\n    pub description: String,\n    #[serde(flatten)]\n    pub action: Action,\n}\n\nimpl Display for Param {\n    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {\n        write!(f, \"🪃 {} {}\", self.name, self.action)\n    }\n}\n\nimpl Param {\n    pub fn set_value(&mut self, val: &Value) {\n        match &mut self.action {\n            Action::Replace { value, .. } => {\n                *value = val.as_str().map(|s| s.to_string());\n            }\n            Action::Retain { value, .. } => {\n                *value = val.as_bool();\n            }\n        }\n    }\n\n    /// Prompt the user for a value for this [Param] using inquire crate.\n    pub fn set_value_by_prompting(&mut self) -> anyhow::Result<()> {\n        match &mut self.action {\n            Action::Replace { placeholder, value } => {\n                let mut p = inquire::Text::new(&self.description).with_placeholder(placeholder);\n                if let Some(def) = value.as_ref() {\n                    p = p.with_default(def);\n                }\n\n                let to = p.prompt()?;\n                if !to.is_empty() {\n                    *value = Some(to);\n                }\n            }\n            Action::Retain { paths: _, value } => {\n                let mut p = inquire::Confirm::new(&self.description);\n                if let Some(def) = value {\n                    p = p.with_default(*def);\n                }\n\n                let v = p.prompt()?;\n                *value = Some(v)\n            }\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/omnix-init/src/registry.rs",
    "content": "use lazy_static::lazy_static;\nuse std::{collections::HashMap, path::Path};\n\nuse nix_rs::{\n    command::{NixCmd, NixCmdError},\n    flake::{command::FlakeOptions, eval::nix_eval, url::FlakeUrl},\n};\nuse tokio::sync::OnceCell;\n\nlazy_static! {\n    /// The registry flake\n    pub static ref OM_INIT_REGISTRY: FlakeUrl = {\n        let path = env!(\"OM_INIT_REGISTRY\");\n        Into::<FlakeUrl>::into(Path::new(path)).with_attr(\"registry\")\n    };\n}\n\n/// Our builtin registry of templates\nstatic BUILTIN_REGISTRY: OnceCell<Result<Registry, NixCmdError>> = OnceCell::const_new();\n\n#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]\npub struct Registry(pub HashMap<String, FlakeUrl>);\n\npub async fn get(nixcmd: &NixCmd) -> &'static Result<Registry, NixCmdError> {\n    BUILTIN_REGISTRY\n        .get_or_init(|| async {\n            let registry =\n                nix_eval::<Registry>(nixcmd, &FlakeOptions::default(), &OM_INIT_REGISTRY).await?;\n            Ok(registry)\n        })\n        .await\n}\n"
  },
  {
    "path": "crates/omnix-init/src/template.rs",
    "content": "use std::{\n    collections::{BTreeMap, HashMap},\n    path::{Path, PathBuf},\n};\n\nuse anyhow::Context;\nuse itertools::Itertools;\nuse serde::Deserialize;\nuse serde_json::Value;\n\nuse crate::param;\n\n/// A template in the `om.templates` config\n#[derive(Debug, Deserialize, Clone)]\npub struct Template {\n    pub template: NixTemplate,\n    pub params: Vec<param::Param>,\n    #[serde(default)]\n    pub tests: BTreeMap<String, super::test::OmInitTest>,\n}\n\n/// The official Nix template (`flake.templates.<name>`)\n#[derive(Debug, Deserialize, Clone)]\npub struct NixTemplate {\n    pub path: PathBuf,\n    pub description: Option<String>,\n    #[serde(rename = \"welcomeText\")]\n    pub welcome_text: Option<String>,\n}\n\nimpl Template {\n    /// Scaffold the [Template] at the given path.\n    //\n    /// Returns the canonicalized path of the output directory\n    pub async fn scaffold_at(&self, out_dir: &Path) -> anyhow::Result<PathBuf> {\n        // Recursively copy the self.template.path to the output directory\n        omnix_common::fs::copy_dir_all(&self.template.path, out_dir)\n            .await\n            .with_context(|| \"Unable to copy files\")?;\n\n        // Do param replacements\n        self.apply_actions(out_dir).await?;\n\n        out_dir\n            .canonicalize()\n            .with_context(|| \"Unable to canonicalize path\")\n    }\n\n    /// Set 'default' fields of prompts to the user-defined values\n    ///\n    /// Given a list of prompts, and the user-defined default values for a subset of them (as JSON-parsed `HashMap<String, Value>` where String is the prompt name and serde 'Value' is the 'default' field of action), mutate the prompts to set those 'default' fields\n    pub fn set_param_values(&mut self, values: &HashMap<String, Value>) {\n        for param in self.params.iter_mut() {\n            if let Some(v) = values.get(&param.name) {\n                param.set_value(v);\n            }\n        }\n    }\n\n    async fn apply_actions(&self, out_dir: &Path) -> anyhow::Result<()> {\n        for param in self.params.iter().sorted_by(|a, b| a.action.cmp(&b.action)) {\n            if param.action.has_value() {\n                tracing::info!(\"{}\", param);\n            }\n            param\n                .action\n                .apply(out_dir.as_ref())\n                .await\n                .with_context(|| format!(\"Unable to apply param {}\", param.name))?;\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "crates/omnix-init/src/test.rs",
    "content": "use std::{\n    collections::HashMap,\n    path::{Path, PathBuf},\n};\n\nuse anyhow::Context;\nuse nix_rs::{\n    command::NixCmd,\n    flake::{command::FlakeOptions, system::System, url::FlakeUrl},\n};\nuse serde::Deserialize;\nuse serde_json::Value;\n\nuse crate::config::FlakeTemplate;\n\n/// A test for a single template\n#[derive(Debug, Deserialize, Clone)]\npub struct OmInitTest {\n    /// The template name to pass to `om init`\n    /// MAKES NO SENSE\n    /// template_name: FlakeUrl,\n    /// The --default-params to pass to `om init`\n    params: HashMap<String, Value>,\n    /// Various assertions to make after running `om init`\n    asserts: Asserts,\n    /// Optional, whitelist of systems to run this test on (others are ignored)\n    systems: Option<Vec<System>>,\n}\n\nimpl OmInitTest {\n    /// Can this test be run on this system?\n    pub fn can_run_on(&self, system: &System) -> bool {\n        if let Some(systems) = self.systems.as_ref() {\n            systems.contains(system)\n        } else {\n            true\n        }\n    }\n\n    /// Run this test on a temporary directory\n    pub async fn run_test(\n        &self,\n        url: &FlakeUrl,\n        template: &FlakeTemplate<'_>,\n    ) -> anyhow::Result<()> {\n        let temp_dir = assert_fs::TempDir::new().unwrap();\n        let out_dir = temp_dir.path().join(\"output\");\n        let mut template = template.clone();\n\n        tracing::info!(\n            \"🧪 [{:?}] Running test params={:?} systems-whitelist={}\",\n            &url,\n            &self.params,\n            self.systems\n                .as_ref()\n                .map(|systems| systems\n                    .iter()\n                    .map(|s| s.to_string())\n                    .collect::<Vec<_>>()\n                    .join(\",\"))\n                .unwrap_or_else(|| \"all\".to_string())\n        );\n        template.template.set_param_values(&self.params);\n        template\n            .template\n            .scaffold_at(&out_dir)\n            .await\n            .with_context(|| \"Unable to scaffold\")?;\n\n        // Recursively print the contents of out_dir to debug test failures\n        let paths = omnix_common::fs::find_paths(&out_dir).await?;\n        tracing::debug!(\n            \"Template files (under {}): {}\",\n            out_dir.display(),\n            paths\n                .iter()\n                .map(|path| path.display().to_string())\n                .collect::<Vec<_>>()\n                .join(\"; \")\n        );\n\n        // Run assertion tests\n        self.asserts.assert(&out_dir).await?;\n\n        temp_dir.close().unwrap();\n        Ok(())\n    }\n}\n\n#[derive(Debug, Deserialize, Clone, Default)]\npub struct Asserts {\n    /// [PathAsserts] for the source directory\n    #[serde(default)]\n    source: PathAsserts,\n\n    /// [PathAsserts] for `nix build .#<name>`'s out path\n    #[serde(default)]\n    packages: HashMap<String, PathAsserts>,\n}\n\nimpl Asserts {\n    async fn assert(&self, dir: &Path) -> anyhow::Result<()> {\n        self.source.assert(dir);\n\n        for (attr, package) in self.packages.iter() {\n            let paths = nix_rs::flake::command::build(\n                &NixCmd::default(),\n                &FlakeOptions {\n                    current_dir: Some(dir.to_path_buf()),\n                    ..Default::default()\n                },\n                FlakeUrl::from(PathBuf::from(\".\")).with_attr(attr),\n            )\n            .await?;\n            assert_matches!(paths.first().and_then(|v| v.first_output()), Some(path) => {\n                package.assert(path);\n            });\n        }\n\n        Ok(())\n    }\n}\n\n/// Set of path assertions to make\n///\n/// If value is true, the path must exist.\n#[derive(Debug, Deserialize, Clone, Default)]\npub struct PathAsserts(HashMap<String, bool>);\n\nimpl PathAsserts {\n    fn assert(&self, dir: &Path) {\n        for (path, must_exist) in self.0.iter() {\n            println!(\"PathAssert {}; exist? ({}) in {:?}\", path, must_exist, dir);\n            let check = dir.join(path).exists();\n            let verb = if *must_exist { \"exist\" } else { \"not exist\" };\n            assert!(\n                if *must_exist { check } else { !check },\n                \"Expected path to {}: {:?} (under {:?})\",\n                verb,\n                path,\n                dir,\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "doc/.gitignore",
    "content": "/book"
  },
  {
    "path": "doc/config.md",
    "content": "---\nshort-title: Configuration\n---\n\n# Omnix Configuration\n\nYou can configure Omnix's behaviour on your repository by creating a top-level `om.yaml` file.  If there is no `om.yaml` file, Omnix will evaluate the flake's `.#om` output instead. Prefer creating a `om.yaml` in general, as it is faster to read than evaluating complex Nix flakes.\n\n> [!NOTE]\n> For am example `om.yaml`, see [that of omnix itself](https://github.com/juspay/omnix/blob/main/om.yaml)"
  },
  {
    "path": "doc/flake.nix",
    "content": "{\n  inputs = {\n    emanote.url = \"github:srid/emanote\";\n    emanote.inputs.emanote-template.follows = \"\";\n    nixpkgs.follows = \"emanote/nixpkgs\";\n    flake-parts.follows = \"emanote/flake-parts\";\n  };\n\n  outputs = inputs@{ self, flake-parts, nixpkgs, ... }:\n    flake-parts.lib.mkFlake { inherit inputs; } {\n      systems = nixpkgs.lib.systems.flakeExposed;\n      imports = [ inputs.emanote.flakeModule ];\n      perSystem = { self', pkgs, system, ... }: {\n        emanote = {\n          sites.\"default\" = {\n            layers = [{ path = ./.; pathString = \".\"; }];\n            baseUrl = \"/\"; # Keep URLs the same as mdBook\n            prettyUrls = true;\n          };\n        };\n      };\n    };\n}\n"
  },
  {
    "path": "doc/history.md",
    "content": "---\norder: 100\n---\n\n# Release history\n\n## 1.3.2 (2026-01-06) {#1.3.2}\n\n### Bumps\n\n- `syntect`: `5.3.2` -> `5.3.3`\n\n## 1.3.1 (2025-12-18) {#1.3.1}\n\n### Fixes\n\n- `om health`: Support `major.minor.patch+patch_count` (ex: `2.31.2+1`) versioning scheme of nix from nixpkgs.\n\n## 1.3.0 (2025-07-15) {#1.3.0}\n\n- `om ci`: Allow impure builds through `impure = true;` setting in `om.yaml` (#445)\n- `om health`\n  - Fix DetSys installer hijacking its own version into `nix --version` causing false Nix version detection. (#458)\n  - Add homebrew check (disabled by default) (#459)\n\n## 1.0.3 (2025-03-17) {#1.0.3}\n\n### Fixes\n\n- `om ci`\n  - Extra nix handling\n      - Allow `--override-input` to work again (#439)\n      - Support `--rebuild` by disallowing it in irrelevant subcommands (`eval`, `develop`, `run`, `flake {lock,check}`) (#441)\n- `om init`\n  - Handle symlinks *as is* (we expect relative symlink targets) without resolution (#443)\n\n## 1.0.2 (2025-03-11) {#1.0.2}\n\n### Fixes\n\n- `om ci`\n  - Prevent bad UTF-8 in build logs from crashing `om ci run` (#437)\n\n## 1.0.1 (2025-03-10) {#1.0.1}\n\n### Fixes\n\n- `om init`\n  - now copies over permissions as is (e.g.: respects executable bits on files) (#434)\n  - applies replace in proper order so that directory rename doesn't skip content replace in its children  (#435)\n\n### Chores\n\n- Allow building on stable version of Rust (#427)\n- Define ENVs in a single place and import them as default for all crates (#430)\n\n## 1.0.0 (2025-02-17) {#1.0.0}\n\n### Enhancements\n\n- `om develop`: New command\n- `om init`\n  - Initial working version of `om init` command\n- `om health`\n  - Display Nix installer used (supports DetSys installer)\n  - Display information in Markdown\n  - Remove RAM/disk space checks, moving them to \"information\" section\n  - Add shell check, to ensure its dotfiles are managed by Nix.\n  - Add `--json` that returns the health check results as JSON\n  - Switch from `nix-version.min-required` to more flexible `nix-version.supported`.\n- `om ci`\n  - Support for remote builds over SSH (via `--on` option)\n  - Support for CI steps\n    - Run `nix flake check` on all subflakes (#200)\n    - Ability to add a custom CI step. For example, to run arbitrary commands.\n  - Add `--accept-flake-config`\n  - Add `--results=FILE` to store CI results as JSON in a file\n  - Misc\n    - Avoid running `nix-store` command multiple times (#224)\n    - Locally cache `github:nix-systems` (to avoid Github API rate limit)\n\n### Fixes\n\n- `om ci run`: The `--override-input` option mandated `flake/` prefix (nixci legacy) which is no longer necessary in this release.\n- `om health`: Use `whoami` to determine username which is more reliable than relying on `USER` environment variable\n\n### Backward-incompatible changes\n\n- `nix-health` and `nixci` flake output configurations are no longer supported.\n- `om ci build` has been renamed to `om ci run`.\n\n## 0.1.0 (2024-08-08) {#0.1.0}\n\nInitial release of omnix.\n"
  },
  {
    "path": "doc/index.md",
    "content": "# Omnix\n\n**Omnix** aims to supplement the [Nix](https://nixos.asia/en/nix) CLI to improve developer experience. The project is developed [on Github](https://github.com/juspay/omnix).\n\n<p style=\"text-align: center\">\n<img src=\"favicon.svg\" alt=\"Omnix Logo\" width=\"32px\" />\n</p>\n\n## Install\n\nTo install Omnix, [install Nix](https://nixos.asia/en/install), then run:\n\n```sh\n# Install omnix (using om.cachix.org Nix cache)\nnix --accept-flake-config profile install github:juspay/omnix\n\n# Make sure that the `om` command works\nom --help\n```\n\n## Navigation\n\n### Reference\n- [[om]] - CLI Commands\n  - [[om/ci]] - CI\n  - [[om/health]] - Health\n  - [[om/develop]] - Develop  \n  - [[om/show]] - Show\n  - [[om/init]] - Init\n- [[config]] - Configuration\n\n### Meta\n- [[history]] - Release history\n\n## Discussion\n\nFor discussing Omnix, use [Github Discussions](https://github.com/juspay/omnix/discussions) or [Zulip](https://nixos.zulipchat.com/#narrow/stream/415454-omnix).\n"
  },
  {
    "path": "doc/index.yaml",
    "content": "# Emanote configuration for Omnix documentation\n# Ref: https://github.com/srid/emanote/blob/master/emanote/default/index.yaml\n\ntemplate:\n\n  # https://tailwindcss.com/docs/customizing-colors#default-color-palette\n  theme: fuchsia\n\n  editBaseUrl: https://github.com/juspay/omnix/edit/main/doc\n  \n  sidebar:\n    collapsed: false\n\n  uptree: \n    enable: true\n  sidebar: \n    enable: false\n  breadcrumbs: \n    enable: false\n  toc:\n    enable: false\n  base:\n    containerClass: container mx-auto max-w-prose\n  \n  # Use pretty URLs to maintain compatibility with mdBook structure\n  # urlStrategy: pretty\n\npage:\n  siteTitle: \"Omnix documentation\"\n  siteUrl: https://omnix.page\n  headHtml: |\n    <snippet var=\"js.highlightjs\" />"
  },
  {
    "path": "doc/mod.just",
    "content": "default:\n  @just --list doc\n\n# Run emanote live server\nrun:\n  nix run\n\n# Do link checks on docs\ncheck:\n    nix run .#linkCheck\n"
  },
  {
    "path": "doc/om/ci.md",
    "content": "# `om ci`\n\n`om ci` runs continuous integration (CI)-friendly builds for your project. It builds all outputs in the flake, or optionally its [sub-flakes](https://github.com/hercules-ci/flake-parts/issues/119). You can run `om ci` locally or in an actual CI envirnoment, like GitHub Actions. Using [devour-flake] it will automatically build the following outputs:\n\n| Type                   | Output Key                                      |\n| ---------------------- | ----------------------------------------------- |\n| Standard flake outputs | `packages`, `apps`, `checks`, `devShells`       |\n| NixOS                  | `nixosConfigurations.*`                         |\n| nix-darwin             | `darwinConfigurations.*`                        |\n| home-manager           | `legacyPackages.${system}.homeConfigurations.*` |\n\nA `result` symlink is also produced, containing a JSON of all built paths. See [here](#out-link).\n\n## Basic Usage {#usage}\n\n`om ci run` accepts any valid [flake URL](https://nixos.asia/en/flake-url) or a Github PR URL.\n\n```sh\n# Run CI on current directory flake\n$ om ci # Or `om ci run` or `om ci run .`\n\n# Run CI on a local flake (default is $PWD)\n$ om ci run ~/code/myproject\n\n# Pass custom arguments to `nix` after '--'\n$ om ci run ~/code/myproject -- --accept-flake-config\n\n# Run CI on a github repo\n$ om ci run github:hercules-ci/hercules-ci-agent\n\n# Run CI on a github PR\n$ om ci run https://github.com/srid/emanote/pull/451\n\n# Run CI only the selected sub-flake\n$ git clone https://github.com/srid/haskell-flake && cd haskell-flake\n$ om ci run .#default.dev\n\n# Run CI remotely over SSH\n$ om ci run --on ssh://myname@myserver ~/code/myproject\n```\n\n## Results JSON and closure {#out-link}\n\nJust like `nix build`, `om ci` will produce a `result` symlink that contains a JSON of all store paths built. Use options `--out-link <PATH>` and `--no-link` to control this behaviour.\n\nAs long as this symlink exists, your built paths will survive garbage collection, because the closure of this symlink contains the entire build closure.\n\nNote that in order to include all build dependencies, you should pass `--include-all-dependencies`, viz.:\n\n```\nom ci run --include-all-dependencies | xargs cachix push mycache\n```\n\nThe above command will push the *entire* build closure (runtime and build dependencies) to the given cache.\n\n## Using in Github Actions {#gh}\n\nIn addition to serving the purpose of being a \"local CI\", `om ci` can be used in Github Actions to enable CI for your GitHub repositories.\n\n### Standard Runners {#gh-simple}\n\nAdd this to your workflow file (`.github/workflows/ci.yml`) to build all flake outputs using GitHub provided runners:\n\n```yaml\n      - uses: actions/checkout@v4\n      - uses: DeterminateSystems/nix-installer-action@main\n      - name: Install omnix\n        run: nix profile install nixpkgs#omnix\n      - run: om ci\n```\n\n### Self-hosted Runners with Job Matrix {#gh-matrix}\n\nHere's a more advanced example that configures a job matrix. This is useful when you want to run the CI on multiple systems (e.g. `aarch64-linux`, `aarch64-darwin`), each captured as a separate job by GitHub, as shown in the screenshot below. It also, incidentally, demonstrates how to use self-hosted runners.\n\n![](../ci-github-matrix.png)\n\nThe `om ci gh-matrix` command outputs the matrix JSON for creating [a matrix of job variations](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow). An example configuration, using self-hosted runners, is shown below.\n\n> [!NOTE]\n> This currently requires an explicit [CI configuration](../config.md) in your flake, setting `om.ci.default.root.dir` to `.`.\n\n```yaml\n# Run on aarch64-linux and aarch64-darwin\njobs:\n  configure:\n    runs-on: x86_64-linux\n    outputs:\n      matrix: ${{ steps.set-matrix.outputs.matrix }}\n    steps:\n     - uses: actions/checkout@v4\n     - id: set-matrix\n       run: |\n         set -euxo pipefail\n         MATRIX=\"$(om ci gh-matrix --systems=x86_64-linux,aarch64-darwin | jq -c .)\"\n         echo \"matrix=$MATRIX\" >> $GITHUB_OUTPUT\n  nix:\n    runs-on: ${{ matrix.system }}\n    needs: configure\n    strategy:\n      matrix: ${{ fromJson(needs.configure.outputs.matrix) }}\n      fail-fast: false\n    steps:\n      - uses: actions/checkout@v4\n      - run: om ci run --systems \"${{ matrix.system }}\" \".#default.${{ matrix.subflake }}\"\n```\n\n> [!TIP]\n> If your builds fail due to GitHub's rate limiting, consider passing `--extra-access-tokens` (see [an example PR](https://github.com/srid/nixos-flake/pull/55)).\n\n## Configuring {#config}\n\nBy default, `om ci` will build the top-level flake, but you can tell it to build sub-flakes (here, `./dir1` and `./dir2`) by adding the following to your [Om configuration](../config.md):\n\n```nix\n# myproject/flake.nix\n{\n  om.ci.default = {\n    dir1 = {\n      dir = \"dir1\";\n    };\n    dir2 = {\n      dir = \"dir2\";\n      overrideInputs.myproject = ./.;\n    };\n  }\n}\n```\n\nYou can have more than one CI configuration. For eg., `om ci run .#foo` will run the configuration from `om.ci.foo` flake output.\n\n### Custom CI actions {#custom}\n\nYou can define custom CI actions in your flake, which will be run as part of `om ci run`. For example, to run tests in the nix develop shell:\n\n```nix\n{\n  om.ci.default = {\n    root = {\n      dir = \".\";\n      steps = {\n        # The build step is enabled by default. It builds all flake outputs.\n        build.enable = true;\n        # Other steps include: lockfile & flake-check\n\n        # Users can define custom steps to run any arbitrary flake app or devShell command.\n        custom = {\n          # Here, we run cargo tests in the nix shell\n          # This equivalent to `nix develop .#default -c cargo test`\n          cargo-test = {\n            type = \"devshell\";\n            # name = \"default\"\n            command = [ \"cargo\" \"test\" ];\n          };\n\n          # We can also flake apps\n          # This is equivalent to `nix run .#check-closure-size`\n          closure-size = {\n            type = \"app\";\n            name = \"check-closure-size\";\n          };\n        };\n      };\n    };\n  };\n}\n```\n\nFor a real-world example of custom steps, checkout [Omnix's configuration](https://github.com/juspay/omnix/blob/5322235ce4069e72fd5eb477353ee5d1f5100243/nix/modules/om.nix#L16-L33).\n\n## Remote CI {#remote}\n\nOmnix can run CI over SSH.\n\n```sh\nom ci run --on ssh://myname@myserver ~/code/myproject\n```\n\nWhat this does:\n\n1. Copy the flake source to the remote server, and run `om ci` there\n2. Copy the built paths back to local store\n\n### Options\n\n- Pass `copy-inputs=true` if you wish to copy all flake inputs recursively. This is useful if you have private Git inputs. For example, `om ci run --on \"ssh://myname@myserver?copy-inputs=true\" ~/code/myproject`\n- Omnix copies the results back to local store, unless `--no-link` was passed.\n\n## Examples\n\nSome real-world examples of how `om ci` is used with specific configurations:\n\n- [omnix](https://github.com/juspay/omnix/blob/5322235ce4069e72fd5eb477353ee5d1f5100243/nix/modules/om.nix#L16-L33)\n- [services-flake](https://github.com/juspay/services-flake/blob/197fc1c4d07d09f4e01dd935450608c35393b102/flake.nix#L10-L24)\n- [nixos-flake](https://github.com/srid/nixos-flake/blob/4af32875e7cc6df440c5f5cf93c67af41902768b/flake.nix#L29-L45)\n- [haskell-flake](https://github.com/srid/haskell-flake/blob/d128c7329bfc73c3eeef90f6d215d0ccd7baf78c/flake.nix#L15-L67)\n    - Here's [a blog post](https://twitter.com/sridca/status/1763528379188265314) that talks about how it is used in haskell-flake\n- [superposition](https://github.com/juspay/superposition/blob/5eeb498cb351d958c923874afafe8a21127ac8ce/nix/om.nix)\n- [haskell-rust-ffi-template](https://github.com/shivaraj-bh/haskell-rust-ffi-template/blob/f38b383b2a12afcb069ef38142faa99bb4b726f4/nix/om.nix)\n\n## What it does {#mech}\n\n- Check that the Nix version is not tool old (using [`om health`](health.md))\n- Determine the list of flakes in the repo to build\n  - By default, this is the root flake.\n  - The user can also explicitly specify multiple sub-flakes in `om.ci.default` output of their root flake.\n- For each (sub)flake identified, `om ci run` will run the following steps:\n    - Check that `flake.lock` is up to date, if applicable.\n    - Build all flake outputs, using [devour-flake](https://github.com/srid/devour-flake)[^schema]\n      - Then, print the built store paths to stdout\n    - If the `flake-check` step is enabled ([example](https://github.com/juspay/omnix/pull/376/files)), run `nix flake check`\n    - Run user defined [custom steps](#custom)\n\n[^schema]: Support for [flake-schemas](https://github.com/srid/devour-flake/pull/11) is planned\n\n[devour-flake]: https://github.com/srid/devour-flake\n\n## See also\n\n- [github-nix-ci](https://github.com/juspay/github-nix-ci) - A simple NixOS & nix-darwin module for self-hosting GitHub runners\n- [jenkins-nix-ci](https://github.com/juspay/jenkins-nix-ci) - Jenkins NixOS module that supports `nixci` (predecessor of `om ci`) as a Groovy function\n- [cachix-push](https://github.com/juspay/cachix-push) - A flake-parts module that provides an app to enable whitelisted pushing and pinning of store paths to cachix.\n"
  },
  {
    "path": "doc/om/develop/omnixrc/v1",
    "content": "# -*- mode: sh -*-\n# shellcheck shell=bash\n\n# A flag to control whether to use omnix from latest github repo or from nixpkgs\nOMNIX_FROM_NIXPKGS=\"${OMNIX_FROM_NIXPKGS:-true}\"\n\n# Define OMNIX_BIN based on the flag\nif [[ \"${OMNIX_FROM_NIXPKGS}\" == \"true\" ]]; then\n  OMNIX_BIN=\"${OMNIX_BIN:-\"nix run github:nixos/nixpkgs/4f3a8e6936bb8d3490057ed114738ccb7af6b73b#omnix --\"}\"\nelse\n  # Fetch omnix only through binary cache from GitHub\n  # Hence `-j0`, as well as `--option builders ''` for when there are remote builders.\n  OMNIX_BIN=\"${OMNIX_BIN:-\"nix --accept-flake-config --option builders '' -j0 run github:juspay/omnix --\"}\"\nfi\n\nuse_omnix() {\n  echo \"Invoking omnix using: ${OMNIX_BIN}\"\n\n  # Run pre-shell stage, which runs health checks and applies changes as necessary.\n  ${OMNIX_BIN} develop --stage=pre-shell $* || exit 1\n\n  # Defer to nix-direnv now.\n  use flake ${*:-.} --accept-flake-config\n\n  if [[ ! -z \"${NIX_DIRENV_DID_FALLBACK:-}\" ]]; then\n    # Nix shell failed (direnv fell back to previous instance); move on!\n    exit\n  else\n    # Run post-shell stage (prints 'readme')\n    ${OMNIX_BIN} develop --stage=post-shell $*\n  fi\n}\n"
  },
  {
    "path": "doc/om/develop.md",
    "content": "# `om develop`\n\nThe `om develop` command should be used indirectly in direnv, via the `use omnix` directive in your `.envrc`.\n\n## Getting started {#start}\n\n1. Put this in your `.envrc` file:\n\n    ```sh\n    source_url \\\n      https://omnix.page/om/develop/omnixrc/v1 \\\n      'sha256-FBAVRYkaexKeFKQGUxaPHqhBnqA7km7++O77dKiyD0I='\n    watch_file om.yaml\n    use omnix\n    ```\n\n2. You should also create a [`om.yaml`](../config.md) file in your project to avoid Nix evaluation:\n\n    ```sh\n    touch om.yaml # Can be empty\n    ```\n\n3. Optionally, add a welcome text (see below) to that `om.yaml`. See [this commit](https://github.com/srid/haskell-template/commit/128105dbeac47c515065ba377f4b1f976ec4f696) for a full example.\n\n## What does it do? {#what}\n\n`use omnix` wraps `use flake` (of [nix-direnv](https://nixos.asia/en/direnv)) providing additional capabilities:\n\n- Run [`om health`](health.md) to check the health of the Nix environment.\n  - Run `cachix use` automatically if the project uses cachix.\n- Print a welcome text after spawning the Nix devshell.\n\nThe ideal goal here being that `cd`'ing to a project should do everything necessary to get you started immediately.\n\n## Welcome text {#welcome}\n\nThe welcome text can be configured in your [`om.yaml`](../config.md) file. For example:\n\n```yaml\ndevelop:\n  default:\n    readme: |\n      🍾 Welcome to the **omnix** project\n\n      To run omnix,\n\n      ```sh-session\n      just watch <args>\n      ```\n\n      (Now, as you edit the Rust sources, the above will reload!)\n\n      🍎🍎 Run 'just' to see more commands. See <https://nixos.asia/en/vscode> for IDE setup.\n```\n\n## Revision History for `omnixrc` {#omnixrc-history}\n\n### v1\n\n- Initial release using pinned nixpkgs for omnix `1.3.0`.\n"
  },
  {
    "path": "doc/om/health.md",
    "content": "# `om health`\n\nThe `om health` command checks the health of your Nix install. Furthermore, individual projects can configure their own health checks in their `flake.nix`. For example, the nammayatri project checks that [the cachix cache is in use][ny-cache].\n\n[ny-cache]: https://github.com/nammayatri/nammayatri/blob/e25e0931e698a4eb369da0eba7f5a02dd03f59a6/om.yaml#L4-L6\n\n\n![](https://github.com/juspay/omnix/assets/3998/abbbc54b-d888-42fb-a2a8-31d9ae142d6a)\n\n\n## Checks performed\n\n| Check                                  | Configurable in `flake.nix`? |\n| -------------------------------------- | ---------------------------- |\n| Flakes are enabled                     | -                            |\n| Nix version is supported               | Yes                          |\n| Nix runs natively (no rosetta)[^ros]   | Yes                          |\n| Builds use multiple cores (`max-jobs`) | Yes                          |\n| Nix Caches in use                      | Yes                          |\n| Direnv: installed and activated        | Yes                          |\n| Dotfiles are managed by Nix            | Yes                          |\n| Min RAM / Disk space                   | Yes                          |\n| Homebrew install detection (disabled by default)[^macos] | Yes                |\n\n[^ros]: This check is only performed on macOS with Apple Silicon.\n[^macos]: This check is only performed on  macOS.\n\nNote that some checks are considered non-essential. For eg., the disk space check looks for 1TB+ disk space, but if the user is on a laptop with 256GB SSD, the check will report a warning instead of failing. This can also be configured in per-project basis from `flake.nix` (see below).\n\n## Usage\n\n```bash\nom health\n```\n\nTo run use the health check configuration specified in a project flake, pass that flake as an argument. For eg., to run halth checks defined from the nammayatri project, run:\n\n```bash\n# The argument can be any flake URL (including a local path)\nom health github:nammayatri/nammayatri\n```\n\n## Per-project configuration {#conf}\n\nTo add project specific health checks or configure health checks, add the following to your [`om.yaml`](../config.md):\n\n```yaml\nhealth:\n  default:\n    caches:\n      required:\n        - \"https://ourproject.cachix.org\"\n```\n\nTo see all available configuration options, run `om health --dump-schema`. This will dump the schema of the configuration in JSON format. Convert that to YAML to see what can be added under the `om.health.default` key of your [`om.yaml`](../config.md).\n\n```sh-session\n$ om health --dump-schema | nix run nixpkgs#yq-go -- -P\n```\n\nThis will output:\n\n```yaml\nflake-enabled: {}\nnix-version:\n  supported: '>=2.16.0'\nrosetta:\n  enable: true\n  required: true\nmax-jobs: {}\ntrusted-users:\n  enable: false\ncaches:\n  required:\n    - https://cache.nixos.org/\ndirenv:\n  enable: true\n  required: false\nhomebrew:\n  enable: false\n  required: false\nshell:\n  enable: true\n  required: false\n```\n\n### Adding devShell check {#devshell}\n\nYou can automatically run `om health` as part of direnv invocation; see [`om develop`](develop.md) for details.\n"
  },
  {
    "path": "doc/om/init.md",
    "content": "# `om init`\n\nThe `om init` command provides a better [`nix flake init`](https://nix.dev/manual/nix/2.18/command-ref/new-cli/nix3-flake-init) experience. Specifically, it provides:\n\n1. a registry of flake templates that you can choose from\n2. support for template paramters that can be filled in by the user\n\nTo get started, run:\n\n```sh\nom init -o ~/myproject\n```\n\nThis will prompt you to choose a template from the builtin registry (see below section), and then initialize it in the `myproject` directory.\n\n## Builtin registry {#registry}\n\nThe builtin registry (stored in [a flake][flake]) contains the following templates:\n\n- [Haskell project template](https://github.com/srid/haskell-template)\n- [Rust project template](https://github.com/srid/rust-nix-template)\n- [NixOS/ nix-darwin/ home-manager template](https://github.com/juspay/nixos-unified-template)\n\n[flake]: https://github.com/juspay/omnix/blob/main/crates/omnix-init/registry/flake.nix\n\n## Initializing your own project templates {#custom}\n\nIf your flake provides a `om.templates` output (see below section), then `om init` will recognize it. For example:\n\n```sh\nom init -o ~/myproject github:srid/haskell-flake\n```\n\nBecause haskell-flake has [a `om.templates` output](https://github.com/srid/haskell-flake/blob/31d7f050935f5a543212b7624d245f918ab14275/flake.nix#L16-L26), `om init` will prompt you to fill in the parameters defined in the template and initialize it.\n\nYou can also explicitly specify the template to choose from the flake:\n\n```sh\nom init -o ~/myproject github:srid/haskell-flake#haskell-flake\n```\n\nIf there are multiple templates in the flake (as is the case with the builtin registry), omnix will the prompt the user to choose from them.\n\n## Configuration spec {#spec}\n\nOmnix templates can be defined by adding a `om.template` flake output [configuration](../config.md). This should be an attrset of templates. The value should contain the keys `template` (referring to original flake template) as well as `params`, defined as follows:\n\nThere are two kinds of params. **String params** are defined as follows:\n\n```nix\n{\n  name = \"package-name\";\n  description = \"Name of the Rust package\";\n  placeholder = \"rust-nix-template\";\n}\n```\n\nHere, when prompting for this param, the user-provided value if any will replace the given `placeholder` text across all files in the template.\n\n**Boolean params** are defined as follows:\n\n```nix\n{\n  name = \"vscode\";\n  description = \"Include the VSCode settings folder (./.vscode)\";\n  paths = [ \".vscode\" ];\n  value = true;\n}\n```\n\nHere, if the user enables this param, the path globs specified in `paths` will be retained in the template. Otherwise, the paths will be deleted. The `value` key provides a default value; which key is supported for string params as well.\n\nBoth parameter types are distinguished by the presence of the relevant keys (`placeholder` for string, `paths` for boolean).\n\n## Testing templates {#test}\n\nThe [configuration](../config.md) can also include a `tests` key that defines a list of tests to run on the template. Each test is an attrset with `params` and `asserts` keys that indicates the parameter values to test along with the path existance assertions. [For example](https://github.com/juspay/nixos-unified-template/blob/3c4428ac94a4582a33e6fb3fe18df27bbc1e9eb7/modules/flake-parts/template.nix#L139-L157):\n\n```nix\n{\n  tests = {\n    default = {\n      # systems = [ ]; # Optional whitelist of systems to limit this test to\n      params = {\n        username = \"john\";\n        git-email = \"john@ex.com\";\n        git-name = \"John Doe\";\n        neovim = true;\n      };\n      asserts = {\n        # Path assertion tests under template output\n        source = {\n          # true means the path must exist; false means it must not exist\n          \"modules/home/neovim/default.nix\" = true;\n          \".github/workflows\" = false;\n        };\n        # Path assertion tests under the output of a Nix package\n        packages.\"homeConfigurations.john.activationPackage\" = {\n          \"home-path/bin/nvim\" = true;\n        };\n      };\n    };\n  };\n}\n```\n"
  },
  {
    "path": "doc/om/show.md",
    "content": "# `om show`\n\nThe `om show` command seeks to provide a better `nix flake show` experience.\n\n> [!WARNING]\n> Currently, `om show` is a wrapper on `nix flake show`, but with support for [flake schemas](https://github.com/NixOS/nix/pull/8892). More is planned for `om show`. See [issue \\#162](https://github.com/juspay/omnix/issues/162).\n\n## Usage\n\nRun `om show` on any flake - via URL or local path.\n\n```text\n$ om show github:srid/nixos-config\n[..]\n📦 Packages (nix build github:srid/nixos-config#<name>)\n╭──────────┬───────────────────────────────────────────────────────╮\n│ name     │ description                                           │\n├──────────┼───────────────────────────────────────────────────────┤\n│ activate │ Activate NixOS/nix-darwin/home-manager configurations │\n│ update   │ Update the primary flake inputs                       │\n│ default  │ Activate NixOS/nix-darwin/home-manager configurations │\n╰──────────┴───────────────────────────────────────────────────────╯\n\n🐚 Devshells (nix develop github:srid/nixos-config#<name>)\n╭─────────┬──────────────────────────────────╮\n│ name    │ description                      │\n├─────────┼──────────────────────────────────┤\n│ default │ Dev environment for nixos-config │\n╰─────────┴──────────────────────────────────╯\n\n🔍 Checks (nix flake check)\n╭────────────┬─────────────╮\n│ name       │ description │\n├────────────┼─────────────┤\n│ pre-commit │ N/A         │\n╰────────────┴─────────────╯\n\n🐧 NixOS Configurations (nixos-rebuild switch --flake github:srid/nixos-config#<name>)\n╭────────────┬─────────────╮\n│ name       │ description │\n├────────────┼─────────────┤\n│ gate       │ N/A         │\n│ vixen      │ N/A         │\n│ pureintent │ N/A         │\n╰────────────┴─────────────╯\n\n🍏 Darwin Configurations (darwin-rebuild switch --flake github:srid/nixos-config#<name>)\n╭────────────┬─────────────╮\n│ name       │ description │\n├────────────┼─────────────┤\n│ appreciate │ N/A         │\n╰────────────┴─────────────╯\n\n🔧 NixOS Modules\n╭─────────┬─────────────╮\n│ name    │ description │\n├─────────┼─────────────┤\n│ common  │ N/A         │\n│ default │ N/A         │\n╰─────────┴─────────────╯\n\n🎨 Overlays\n╭─────────┬─────────────╮\n│ name    │ description │\n├─────────┼─────────────┤\n│ default │ N/A         │\n╰─────────┴─────────────╯\n```"
  },
  {
    "path": "doc/om.md",
    "content": "\n# The `om` CLI\n\nThe Omnix CLI currently provides a fully-functioning [[ci]] and [[health]] commands. The [[show]] command has basic functionality, whereas the [[init]] command can be used to scaffold new projects using Nix. The [[develop]] command enriches flakes support when used with `direnv`.\n\n```query\npath:./*\n```"
  },
  {
    "path": "flake.nix",
    "content": "{\n  nixConfig = {\n    extra-substituters = \"https://cache.nixos.asia/oss\";\n    extra-trusted-public-keys = \"oss:KO872wNJkCDgmGN3xy9dT89WAhvv13EiKncTtHDItVU=\";\n  };\n  inputs = {\n    nixpkgs.url = \"github:nixos/nixpkgs/nixpkgs-unstable\";\n    flake-parts.url = \"github:hercules-ci/flake-parts\";\n    flake-parts.inputs.nixpkgs-lib.follows = \"nixpkgs\";\n    systems.url = \"github:nix-systems/default\";\n\n    rust-flake.url = \"github:juspay/rust-flake\";\n    rust-flake.inputs.nixpkgs.follows = \"nixpkgs\";\n    git-hooks.url = \"github:cachix/git-hooks.nix\";\n    git-hooks.flake = false;\n\n    # We cache these inputs locally, and then have Omnix reference the catched inputs.\n    devour-flake.url = \"github:srid/devour-flake\";\n    devour-flake.flake = false;\n    nix-systems-x86_64-darwin.url = \"github:nix-systems/x86_64-darwin\";\n    nix-systems-aarch64-darwin.url = \"github:nix-systems/aarch64-darwin\";\n    nix-systems-x86_64-linux.url = \"github:nix-systems/x86_64-linux\";\n    nix-systems-aarch64-linux.url = \"github:nix-systems/aarch64-linux\";\n    true.url = \"github:boolean-option/true\";\n    false.url = \"github:boolean-option/false\";\n    inspect.url = \"github:juspay/inspect/inventory-for-systems\";\n    inspect.flake = false;\n  };\n\n  outputs = inputs:\n    inputs.flake-parts.lib.mkFlake { inherit inputs; } {\n      systems = import inputs.systems;\n      debug = true;\n\n      # See ./nix/modules/flake/*.nix for the modules that are imported here.\n      imports = with builtins;\n        map\n          (fn: ./nix/modules/flake/${fn})\n          (attrNames (readDir ./nix/modules/flake));\n    };\n}\n"
  },
  {
    "path": "justfile",
    "content": "# Documentation targets\nmod doc\n\ndefault:\n    @just --list\n\n# Run all pre-commit hooks on all files\npca:\n    pre-commit run -a\n\n# Run omnix-cli locally\nwatch *ARGS:\n    bacon --job run -- -- {{ ARGS }}\n\nrun *ARGS:\n    cargo run -p omnix-cli {{ ARGS }}\n\nalias w := watch\n\n# Run CI locally\n[group('ci')]\nci:\n    nix --accept-flake-config run . ci\n\n# Run CI locally in devShell (using cargo)\n[group('ci')]\nci-cargo:\n    cargo run -p omnix-cli -- ci run\n\n# Run CI locally in devShell (using cargo) on a simple flake with subflakes\n[group('ci')]\nci-cargo-ext:\n    cargo run -p omnix-cli -- ci run github:srid/nixos-unified\n\n# Do clippy checks for all crates\n[group('ci-steps')]\nclippy:\n    cargo clippy --release --locked --all-targets --all-features --workspace -- --deny warnings\n\n# Build cargo doc for all crates\n[group('ci-steps')]\ncargo-doc:\n    cargo doc --release --all-features --workspace\n\n# Run cargo test for all crates\n[group('ci-steps')]\ncargo-test:\n    cargo test --release --all-features --workspace\n"
  },
  {
    "path": "nix/envs/default.nix",
    "content": "{ src, lib, cachix, fetchFromGitHub }:\nlib.mapAttrs (_: v: builtins.toString v) {\n  OMNIX_SOURCE = src;\n  CACHIX_BIN = lib.getExe' cachix \"cachix\";\n  OM_INIT_REGISTRY = \"path:${src}/crates/omnix-init/registry\";\n  DEFAULT_FLAKE_SCHEMAS = \"path:${src}/nix/flake-schemas\";\n  FLAKE_METADATA = \"path:${src}/crates/nix_rs/src/flake/functions/metadata\";\n  FLAKE_ADDSTRINGCONTEXT = \"path:${src}/crates/nix_rs/src/flake/functions/addstringcontext\";\n  DEVOUR_FLAKE = fetchFromGitHub {\n    owner = \"srid\";\n    repo = \"devour-flake\";\n    rev = \"9fe4db872c107ea217c13b24527b68d9e4a4c01b\";\n    hash = \"sha256-R7MHvTh5fskzxNLBe9bher+GQBZ8ZHjz75CPQG3fSRI=\";\n  };\n  NIX_SYSTEMS =\n    let\n      x86_64-linux = fetchFromGitHub {\n        owner = \"nix-systems\";\n        repo = \"x86_64-linux\";\n        rev = \"2ecfcac5e15790ba6ce360ceccddb15ad16d08a8\";\n        hash = \"sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=\";\n      };\n      aarch64-linux = fetchFromGitHub {\n        owner = \"nix-systems\";\n        repo = \"aarch64-linux\";\n        rev = \"aa1ce1b64c822dff925d63d3e771113f71ada1bb\";\n        hash = \"sha256-1Zp7TRYLXj4P5FLhQ8jBChrgAmQxR3iTypmWf9EFTnc=\";\n      };\n      x86_64-darwin = fetchFromGitHub {\n        owner = \"nix-systems\";\n        repo = \"x86_64-darwin\";\n        rev = \"db0463cce4cd60fb791f33a83d29a1ed53edab9b\";\n        hash = \"sha256-+xT9B1ZbhMg/zpJqd00S06UCZb/A2URW9bqqrZ/JTOg=\";\n      };\n      aarch64-darwin = fetchFromGitHub {\n        owner = \"nix-systems\";\n        repo = \"aarch64-darwin\";\n        rev = \"75e6c6912484d28ebba5769b794ffa4aff653ba2\";\n        hash = \"sha256-PHVNQ7y0EQYzujQRYoRdb96K0m1KSeAjSrbz2b75S6Q=\";\n      };\n    in\n    builtins.toJSON {\n      inherit x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin;\n    };\n  FALSE_FLAKE = fetchFromGitHub {\n    owner = \"boolean-option\";\n    repo = \"false\";\n    rev = \"d06b4794a134686c70a1325df88a6e6768c6b212\";\n    hash = \"sha256-vLy8GQr0noEcoA+jX24FgUVBA/poV36zDWAUChN3hIY=\";\n  };\n  TRUE_FLAKE = fetchFromGitHub {\n    owner = \"boolean-option\";\n    repo = \"true\";\n    rev = \"6ecb49143ca31b140a5273f1575746ba93c3f698\";\n    hash = \"sha256-L9eyTL7njtPBUYmZRYFKCzQFDgua9U9oE7UwCzjZfl8=\";\n  };\n  INSPECT_FLAKE = fetchFromGitHub {\n    owner = \"juspay\";\n    repo = \"inspect\";\n    rev = \"inventory-for-systems\";\n    hash = \"sha256-GTxRovvYWYn2/LDvjA73YttGuqvtKaOFZfOR9YxtST0=\";\n  };\n}\n"
  },
  {
    "path": "nix/flake-schemas/flake.nix",
    "content": "{\n  inputs = {\n    flake-schemas.url = \"github:DeterminateSystems/flake-schemas\";\n  };\n  outputs = { flake-schemas, ... }:\n    let\n      appsSchema = {\n        version = 1;\n        doc = ''\n          The `apps` output provides commands available via `nix run`.\n        '';\n        inventory = output:\n          flake-schemas.lib.mkChildren (builtins.mapAttrs\n            (system: apps:\n              let\n                forSystems = [ system ];\n              in\n              {\n                inherit forSystems;\n                children =\n                  builtins.mapAttrs\n                    (appName: app: {\n                      inherit forSystems;\n                      evalChecks.isValidApp =\n                        app ? type\n                        && app.type == \"app\"\n                        && app ? program\n                        && builtins.isString app.program;\n                      what = \"app\";\n                      shortDescription = app.meta.description or \"\";\n                    })\n                    apps;\n              })\n            output);\n      };\n      nixosConfigurationsSchema = {\n        version = 1;\n        doc = ''\n          The `nixosConfigurations` flake output defines [NixOS system configurations](https://nixos.org/manual/nixos/stable/#ch-configuration).\n        '';\n        inventory = output: flake-schemas.lib.mkChildren (builtins.mapAttrs\n          (configName: machine:\n            {\n              what = \"NixOS configuration\";\n              derivation = machine.config.system.build.toplevel;\n              forSystems = [ machine.pkgs.stdenv.system ];\n              # Force evaluate this derivation on all systems. See: https://github.com/juspay/omnix/pull/277#discussion_r1760164052\n              evalOnAllSystems = true;\n            })\n          output);\n      };\n      homeConfigurationsSchema = {\n        version = 1;\n        doc = ''\n          The `homeConfigurations` flake output defines [Home Manager configurations](https://github.com/nix-community/home-manager).\n        '';\n        inventory = output: flake-schemas.lib.mkChildren (builtins.mapAttrs\n          (configName: this:\n            {\n              what = \"Home Manager configuration\";\n              derivation = this.activationPackage;\n              forSystems = [ this.activationPackage.system ];\n              evalOnAllSystems = true;\n            })\n          output);\n      };\n      darwinConfigurationsSchema = {\n        version = 1;\n        doc = ''\n          The `darwinConfigurations` flake output defines [nix-darwin configurations](https://github.com/LnL7/nix-darwin).\n        '';\n        inventory = output: flake-schemas.lib.mkChildren (builtins.mapAttrs\n          (configName: this:\n            {\n              what = \"nix-darwin configuration\";\n              derivation = this.system;\n              forSystems = [ this.system.system ];\n              evalOnAllSystems = true;\n            })\n          output);\n      };\n    in\n    {\n      schemas = flake-schemas.schemas // {\n        # Until upstream is merged: https://github.com/DeterminateSystems/flake-schemas/pull/31\n        apps = appsSchema;\n        nixosConfigurations = nixosConfigurationsSchema;\n        homeConfigurations = homeConfigurationsSchema;\n        darwinConfigurations = darwinConfigurationsSchema;\n      };\n    };\n}\n"
  },
  {
    "path": "nix/modules/flake/closure-size.nix",
    "content": "{ ... }:\n\nlet\n\n  maxSize = 1000000 * maxSizeInMB.total;\n  maxSizeInMB = rec {\n    total = omnix + cachix;\n    omnix = 60;\n    cachix = 150;\n  };\nin\n{\n  perSystem = { pkgs, ... }: {\n    apps.check-closure-size = rec {\n      meta.description = program.meta.description;\n      program = pkgs.writeShellApplication {\n        name = \"omnix-check-closure-size\";\n        runtimeInputs = [ pkgs.jq pkgs.bc pkgs.nix ];\n        meta.description = \"Check that omnix's nix closure size remains reasonably small\";\n        text = ''\n          set -o pipefail\n          MAX_CLOSURE_SIZE=${builtins.toString maxSize}\n          CLOSURE_SIZE=$(nix path-info --json -S .#default | jq '.[].closureSize')\n          echo \"Omnix closure size: $CLOSURE_SIZE\"\n          echo \"    Max closure size: $MAX_CLOSURE_SIZE\"\n          if [ \"$CLOSURE_SIZE\" -gt \"$MAX_CLOSURE_SIZE\" ]; then\n              echo \"ERROR: Omnix's nix closure size has increased\"\n              exit 3\n          else\n              echo \"OK: Omnix's nix closure size is within limits\"\n          fi\n        '';\n      };\n    };\n  };\n}\n"
  },
  {
    "path": "nix/modules/flake/devshell.nix",
    "content": "let\n  root = ../../..;\nin\n{\n  imports = [\n    (root + /crates/omnix-health/module/flake-module.nix)\n  ];\n\n  perSystem = { config, self', pkgs, ... }: {\n    devShells.default = pkgs.mkShell {\n      name = \"omnix-devshell\";\n      meta.description = \"Omnix development environment\";\n      inputsFrom = [\n        config.pre-commit.devShell\n        self'.devShells.rust\n      ];\n      inherit (config.rust-project.crates.\"omnix-cli\".crane.args)\n        DEVOUR_FLAKE\n        NIX_SYSTEMS\n        DEFAULT_FLAKE_SCHEMAS\n        FLAKE_METADATA\n        FLAKE_ADDSTRINGCONTEXT\n        INSPECT_FLAKE\n        TRUE_FLAKE\n        FALSE_FLAKE\n        OMNIX_SOURCE\n        OM_INIT_REGISTRY\n        CACHIX_BIN\n        ;\n\n      packages = with pkgs; [\n        just\n        nixd\n        bacon\n        cargo-expand\n        cargo-nextest\n        cargo-audit\n        cargo-workspaces\n        trunk\n      ];\n    };\n  };\n}\n"
  },
  {
    "path": "nix/modules/flake/nixpkgs.nix",
    "content": "{ inputs, ... }:\n{\n  imports = [\n    inputs.rust-flake.flakeModules.nixpkgs\n  ];\n  perSystem = { inputs', config, self', pkgs, lib, system, ... }: {\n    nixpkgs.overlays = [\n      (self: super: {\n        # Configure tailwind to enable all relevant plugins\n        tailwindcss = super.tailwindcss.overrideAttrs\n          (oa: {\n            plugins = [\n              pkgs.nodePackages.\"@tailwindcss/aspect-ratio\"\n              pkgs.nodePackages.\"@tailwindcss/forms\"\n              pkgs.nodePackages.\"@tailwindcss/language-server\"\n              pkgs.nodePackages.\"@tailwindcss/line-clamp\"\n              pkgs.nodePackages.\"@tailwindcss/typography\"\n            ];\n          });\n\n        # Like pkgs.nix, but with flake-schemas implemented.\n        # Using until https://github.com/NixOS/nix/pull/8892 is upstreamed\n        nix-flake-schemas =\n          if pkgs.stdenv.isLinux\n          then inputs'.nix.packages.nix-static\n          else inputs'.nix.packages.default;\n      })\n    ];\n  };\n}\n"
  },
  {
    "path": "nix/modules/flake/pre-commit.nix",
    "content": "{ inputs, ... }:\n{\n  imports = [\n    (inputs.git-hooks + /flake-module.nix)\n  ];\n\n  perSystem = {\n    pre-commit.settings = {\n      hooks = {\n        nixpkgs-fmt.enable = true;\n        rustfmt.enable = true;\n      };\n    };\n  };\n}\n"
  },
  {
    "path": "nix/modules/flake/rust.nix",
    "content": "{ inputs, ... }:\n# Nix module for the Rust part of the project\n#\n# This uses Crane, via https://github.com/juspay/rust-flake\n{\n  imports = [\n    inputs.rust-flake.flakeModules.default\n  ];\n  perSystem = { config, self', pkgs, lib, ... }: {\n    rust-project = {\n      # See /crates/*/crate.nix for the crate-specific Nix configuration\n      crateNixFile = \"crate.nix\";\n\n      # To avoid unnecessary rebuilds, start from cleaned source, and then add the Nix files necessary to `nix run` it. Finally, add any files required by the Rust build.\n      src =\n        let\n          # Like crane's filterCargoSources, but doesn't blindly include all TOML files!\n          filterCargoSources = path: type:\n            config.rust-project.crane-lib.filterCargoSources path type\n            && !(lib.hasSuffix \".toml\" path && !lib.hasSuffix \"Cargo.toml\" path);\n        in\n        lib.cleanSourceWith {\n          src = inputs.self;\n          filter = path: type:\n            filterCargoSources path type\n            || lib.hasSuffix \"registry.json\" path\n            || lib.hasSuffix \"crate.nix\" path\n            || \"${inputs.self}/flake.nix\" == path\n            || \"${inputs.self}/flake.lock\" == path\n            || \"${inputs.self}/rust-toolchain.toml\" == path\n            # Select *only* the non-Rust files necessary to build omnix package.\n            || lib.hasSuffix \"envs/default.nix\" path\n            || lib.hasSuffix \"flake/nixpkgs.nix\" path\n            || lib.hasSuffix \"flake/rust.nix\" path\n            || lib.hasSuffix \"tests/flake.nix\" path\n            || lib.hasSuffix \"tests/flake.lock\" path\n            || lib.hasSuffix \"failing/flake.nix\" path\n            || lib.hasSuffix \"registry/flake.nix\" path\n            || lib.hasSuffix \"registry/flake.lock\" path\n            || lib.hasSuffix \"flake-schemas/flake.nix\" path\n            || lib.hasSuffix \"flake-schemas/flake.lock\" path\n            || lib.hasSuffix \"addstringcontext/flake.nix\" path\n            || lib.hasSuffix \"addstringcontext/flake.lock\" path\n            || lib.hasSuffix \"metadata/flake.nix\" path\n            || lib.hasSuffix \"metadata/flake.lock\" path\n          ;\n        };\n      defaults.perCrate.crane.args = import \"${inputs.self}/nix/envs\" {\n        inherit (config.rust-project) src;\n        inherit (pkgs) cachix fetchFromGitHub lib;\n      };\n    };\n\n    packages =\n      let\n        inherit (config.rust-project) crates;\n      in\n      rec {\n        default = omnix-cli;\n        omnix-cli = crates.\"omnix-cli\".crane.outputs.drv.crate.overrideAttrs (oa: {\n          nativeBuildInputs = (oa.nativeBuildInputs or [ ]) ++ [ pkgs.installShellFiles ];\n          postInstall = ''\n            installShellCompletion --cmd om \\\n              --bash <($out/bin/om completion bash) \\\n              --zsh <($out/bin/om completion zsh) \\\n              --fish <($out/bin/om completion fish)\n          '';\n        });\n      };\n\n    apps.omnix-source-is-buildable.program = pkgs.writeShellApplication {\n      name = \"omnix-source-is-buildable\";\n      runtimeInputs = [\n        pkgs.jq\n      ];\n      text = ''\n        set -e\n        cd ${self'.packages.omnix-cli.OMNIX_SOURCE}\n        # Make sure the drv evaluates (to test that no files are accidentally excluded)\n        nix --accept-flake-config --extra-experimental-features \"flakes nix-command\" \\\n          derivation show \".\" | jq -r '.[].outputs.out.path'\n      '';\n    };\n  };\n}\n"
  },
  {
    "path": "om.yaml",
    "content": "ci:\n  default:\n    omnix:\n      dir: .\n      steps:\n        custom:\n          om-show:\n            type: app\n            args:\n              - show\n              - .\n          binary-size-is-small:\n            type: app\n            name: check-closure-size\n            systems:\n              - x86_64-linux\n          omnix-source-is-buildable:\n            type: app\n            name: omnix-source-is-buildable\n          cargo-tests:\n            type: devshell\n            command:\n              - just\n              - cargo-test\n            systems:\n              - x86_64-linux\n              - aarch64-darwin\n          cargo-clippy:\n            type: devshell\n            command:\n              - just\n              - clippy\n            systems:\n              - x86_64-linux\n              - aarch64-darwin\n          cargo-doc:\n            type: devshell\n            command:\n              - just\n              - cargo-doc\n            systems:\n              - x86_64-linux\n              - aarch64-darwin\n    doc:\n      dir: doc\n    registry:\n      dir: crates/omnix-init/registry\n      steps:\n        build:\n          enable: false\n        # Run `nix flake check` for checking the 'templates' outputs\n        flake-check:\n          enable: true\n    cli-test-dep-cache:\n      dir: crates/omnix-cli/tests\n      steps:\n        lockfile:\n          enable: false\nhealth:\n  default:\n    nix-version:\n      supported: \">=2.16.0\"\n    # TODO: Use nixos.asia attic after https://github.com/juspay/omnix/issues/474\n    # caches:\n    #   required:\n    #     - https://om.cachix.org\n    direnv:\n      required: true\n    homebrew:\n      enable: true\ndevelop:\n  default:\n    readme: |\n      🍾 Welcome to the **omnix** project\n\n      To run omnix,\n\n      ```sh-session\n      just watch <args>\n      ```\n\n      (Now, as you edit the Rust sources, the above will reload!)\n\n      🍎🍎 Run 'just' to see more commands. See <https://nixos.asia/en/vscode> for IDE setup.\n"
  },
  {
    "path": "rust-toolchain.toml",
    "content": "[toolchain]\nchannel = \"stable\"\ntargets = [\"x86_64-unknown-linux-musl\", \"aarch64-unknown-linux-musl\"]\n"
  }
]